This commit is contained in:
2024-12-17 14:36:15 -08:00
parent b2dbf46d28
commit 06d106de53
17731 changed files with 3037186 additions and 144 deletions

View File

@@ -0,0 +1,12 @@
# -*- test-case-name: twisted.web.test -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Twisted Web: HTTP clients and servers, plus tools for implementing them.
Contains a L{web server<twisted.web.server>} (including an
L{HTTP implementation<twisted.web.http>}, a
L{resource model<twisted.web.resource>}), and
a L{web client<twisted.web.client>}.
"""

View File

@@ -0,0 +1,68 @@
# -*- test-case-name: twisted.web.test.test_abnf -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tools for pedantically processing the HTTP protocol.
"""
def _istoken(b: bytes) -> bool:
"""
Is the string a token per RFC 9110 section 5.6.2?
"""
for c in b:
if c not in (
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" # ALPHA
b"0123456789" # DIGIT
b"!#$%&'*+-.^_`|~"
):
return False
return b != b""
def _decint(data: bytes) -> int:
"""
Parse a decimal integer of the form C{1*DIGIT}, i.e. consisting only of
decimal digits. The integer may be embedded in whitespace (space and
horizontal tab). This differs from the built-in L{int()} function by
disallowing a leading C{+} character and various forms of whitespace
(note that we sanitize linear whitespace in header values in
L{twisted.web.http_headers.Headers}).
@param data: Value to parse.
@returns: A non-negative integer.
@raises ValueError: When I{value} contains non-decimal characters.
"""
data = data.strip(b" \t")
if not data.isdigit():
raise ValueError(f"Value contains non-decimal digits: {data!r}")
return int(data)
def _ishexdigits(b: bytes) -> bool:
"""
Is the string case-insensitively hexidecimal?
It must be composed of one or more characters in the ranges a-f, A-F
and 0-9.
"""
for c in b:
if c not in b"0123456789abcdefABCDEF":
return False
return b != b""
def _hexint(b: bytes) -> int:
"""
Decode a hexadecimal integer.
Unlike L{int(b, 16)}, this raises L{ValueError} when the integer has
a prefix like C{b'0x'}, C{b'+'}, or C{b'-'}, which is desirable when
parsing network protocols.
"""
if not _ishexdigits(b):
raise ValueError(b)
return int(b, 16)

View File

@@ -0,0 +1,7 @@
# -*- test-case-name: twisted.web.test.test_httpauth -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
HTTP header-based authentication migrated from web2
"""

View File

@@ -0,0 +1,58 @@
# -*- test-case-name: twisted.web.test.test_httpauth -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
HTTP BASIC authentication.
@see: U{http://tools.ietf.org/html/rfc1945}
@see: U{http://tools.ietf.org/html/rfc2616}
@see: U{http://tools.ietf.org/html/rfc2617}
"""
import binascii
from zope.interface import implementer
from twisted.cred import credentials, error
from twisted.web.iweb import ICredentialFactory
@implementer(ICredentialFactory)
class BasicCredentialFactory:
"""
Credential Factory for HTTP Basic Authentication
@type authenticationRealm: L{bytes}
@ivar authenticationRealm: The HTTP authentication realm which will be issued in
challenges.
"""
scheme = b"basic"
def __init__(self, authenticationRealm):
self.authenticationRealm = authenticationRealm
def getChallenge(self, request):
"""
Return a challenge including the HTTP authentication realm with which
this factory was created.
"""
return {"realm": self.authenticationRealm}
def decode(self, response, request):
"""
Parse the base64-encoded, colon-separated username and password into a
L{credentials.UsernamePassword} instance.
"""
try:
creds = binascii.a2b_base64(response + b"===")
except binascii.Error:
raise error.LoginFailed("Invalid credentials")
creds = creds.split(b":", 1)
if len(creds) == 2:
return credentials.UsernamePassword(*creds)
else:
raise error.LoginFailed("Invalid credentials")

View File

@@ -0,0 +1,56 @@
# -*- test-case-name: twisted.web.test.test_httpauth -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Implementation of RFC2617: HTTP Digest Authentication
@see: U{http://www.faqs.org/rfcs/rfc2617.html}
"""
from zope.interface import implementer
from twisted.cred import credentials
from twisted.web.iweb import ICredentialFactory
@implementer(ICredentialFactory)
class DigestCredentialFactory:
"""
Wrapper for L{digest.DigestCredentialFactory} that implements the
L{ICredentialFactory} interface.
"""
scheme = b"digest"
def __init__(self, algorithm, authenticationRealm):
"""
Create the digest credential factory that this object wraps.
"""
self.digest = credentials.DigestCredentialFactory(
algorithm, authenticationRealm
)
def getChallenge(self, request):
"""
Generate the challenge for use in the WWW-Authenticate header
@param request: The L{IRequest} to with access was denied and for the
response to which this challenge is being generated.
@return: The L{dict} that can be used to generate a WWW-Authenticate
header.
"""
return self.digest.getChallenge(request.getClientAddress().host)
def decode(self, response, request):
"""
Create a L{twisted.cred.credentials.DigestedCredentials} object
from the given response and request.
@see: L{ICredentialFactory.decode}
"""
return self.digest.decode(
response, request.method, request.getClientAddress().host
)

View File

@@ -0,0 +1,236 @@
# -*- test-case-name: twisted.web.test.test_httpauth -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
A guard implementation which supports HTTP header-based authentication
schemes.
If no I{Authorization} header is supplied, an anonymous login will be
attempted by using a L{Anonymous} credentials object. If such a header is
supplied and does not contain allowed credentials, or if anonymous login is
denied, a 401 will be sent in the response along with I{WWW-Authenticate}
headers for each of the allowed authentication schemes.
"""
from zope.interface import implementer
from twisted.cred import error
from twisted.cred.credentials import Anonymous
from twisted.logger import Logger
from twisted.python.components import proxyForInterface
from twisted.web import util
from twisted.web.resource import IResource, _UnsafeErrorPage
@implementer(IResource)
class UnauthorizedResource:
"""
Simple IResource to escape Resource dispatch
"""
isLeaf = True
def __init__(self, factories):
self._credentialFactories = factories
def render(self, request):
"""
Send www-authenticate headers to the client
"""
def ensureBytes(s):
return s.encode("ascii") if isinstance(s, str) else s
def generateWWWAuthenticate(scheme, challenge):
lst = []
for k, v in challenge.items():
k = ensureBytes(k)
v = ensureBytes(v)
lst.append(k + b"=" + quoteString(v))
return b" ".join([scheme, b", ".join(lst)])
def quoteString(s):
return b'"' + s.replace(b"\\", rb"\\").replace(b'"', rb"\"") + b'"'
request.setResponseCode(401)
for fact in self._credentialFactories:
challenge = fact.getChallenge(request)
request.responseHeaders.addRawHeader(
b"www-authenticate", generateWWWAuthenticate(fact.scheme, challenge)
)
if request.method == b"HEAD":
return b""
return b"Unauthorized"
def getChildWithDefault(self, path, request):
"""
Disable resource dispatch
"""
return self
def putChild(self, path, child):
# IResource.putChild
raise NotImplementedError()
@implementer(IResource)
class HTTPAuthSessionWrapper:
"""
Wrap a portal, enforcing supported header-based authentication schemes.
@ivar _portal: The L{Portal} which will be used to retrieve L{IResource}
avatars.
@ivar _credentialFactories: A list of L{ICredentialFactory} providers which
will be used to decode I{Authorization} headers into L{ICredentials}
providers.
"""
isLeaf = False
_log = Logger()
def __init__(self, portal, credentialFactories):
"""
Initialize a session wrapper
@type portal: C{Portal}
@param portal: The portal that will authenticate the remote client
@type credentialFactories: C{Iterable}
@param credentialFactories: The portal that will authenticate the
remote client based on one submitted C{ICredentialFactory}
"""
self._portal = portal
self._credentialFactories = credentialFactories
def _authorizedResource(self, request):
"""
Get the L{IResource} which the given request is authorized to receive.
If the proper authorization headers are present, the resource will be
requested from the portal. If not, an anonymous login attempt will be
made.
"""
authheader = request.getHeader(b"authorization")
if not authheader:
return util.DeferredResource(self._login(Anonymous()))
factory, respString = self._selectParseHeader(authheader)
if factory is None:
return UnauthorizedResource(self._credentialFactories)
try:
credentials = factory.decode(respString, request)
except error.LoginFailed:
return UnauthorizedResource(self._credentialFactories)
except BaseException:
self._log.failure("Unexpected failure from credentials factory")
return _UnsafeErrorPage(500, "Internal Error", "")
else:
return util.DeferredResource(self._login(credentials))
def render(self, request):
"""
Find the L{IResource} avatar suitable for the given request, if
possible, and render it. Otherwise, perhaps render an error page
requiring authorization or describing an internal server failure.
"""
return self._authorizedResource(request).render(request)
def getChildWithDefault(self, path, request):
"""
Inspect the Authorization HTTP header, and return a deferred which,
when fired after successful authentication, will return an authorized
C{Avatar}. On authentication failure, an C{UnauthorizedResource} will
be returned, essentially halting further dispatch on the wrapped
resource and all children
"""
# Don't consume any segments of the request - this class should be
# transparent!
request.postpath.insert(0, request.prepath.pop())
return self._authorizedResource(request)
def _login(self, credentials):
"""
Get the L{IResource} avatar for the given credentials.
@return: A L{Deferred} which will be called back with an L{IResource}
avatar or which will errback if authentication fails.
"""
d = self._portal.login(credentials, None, IResource)
d.addCallbacks(self._loginSucceeded, self._loginFailed)
return d
def _loginSucceeded(self, args):
"""
Handle login success by wrapping the resulting L{IResource} avatar
so that the C{logout} callback will be invoked when rendering is
complete.
"""
interface, avatar, logout = args
class ResourceWrapper(proxyForInterface(IResource, "resource")):
"""
Wrap an L{IResource} so that whenever it or a child of it
completes rendering, the cred logout hook will be invoked.
An assumption is made here that exactly one L{IResource} from
among C{avatar} and all of its children will be rendered. If
more than one is rendered, C{logout} will be invoked multiple
times and probably earlier than desired.
"""
def getChildWithDefault(self, name, request):
"""
Pass through the lookup to the wrapped resource, wrapping
the result in L{ResourceWrapper} to ensure C{logout} is
called when rendering of the child is complete.
"""
return ResourceWrapper(self.resource.getChildWithDefault(name, request))
def render(self, request):
"""
Hook into response generation so that when rendering has
finished completely (with or without error), C{logout} is
called.
"""
request.notifyFinish().addBoth(lambda ign: logout())
return super().render(request)
return ResourceWrapper(avatar)
def _loginFailed(self, result):
"""
Handle login failure by presenting either another challenge (for
expected authentication/authorization-related failures) or a server
error page (for anything else).
"""
if result.check(error.Unauthorized, error.LoginFailed):
return UnauthorizedResource(self._credentialFactories)
else:
self._log.failure(
"HTTPAuthSessionWrapper.getChildWithDefault encountered "
"unexpected error",
failure=result,
)
return _UnsafeErrorPage(500, "Internal Error", "")
def _selectParseHeader(self, header):
"""
Choose an C{ICredentialFactory} from C{_credentialFactories}
suitable to use to decode the given I{Authenticate} header.
@return: A two-tuple of a factory and the remaining portion of the
header value to be decoded or a two-tuple of L{None} if no
factory can decode the header value.
"""
elements = header.split(b" ")
scheme = elements[0].lower()
for fact in self._credentialFactories:
if fact.scheme == scheme:
return (fact, b" ".join(elements[1:]))
return (None, None)
def putChild(self, path, child):
# IResource.putChild
raise NotImplementedError()

View File

@@ -0,0 +1,200 @@
# -*- test-case-name: twisted.web.test.test_template -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
import itertools
from typing import (
TYPE_CHECKING,
Any,
Callable,
List,
Optional,
TypeVar,
Union,
overload,
)
from zope.interface import implementer
from twisted.web.error import (
MissingRenderMethod,
MissingTemplateLoader,
UnexposedMethodError,
)
from twisted.web.iweb import IRenderable, IRequest, ITemplateLoader
if TYPE_CHECKING:
from twisted.web.template import Flattenable, Tag
T = TypeVar("T")
_Tc = TypeVar("_Tc", bound=Callable[..., object])
class Expose:
"""
Helper for exposing methods for various uses using a simple decorator-style
callable.
Instances of this class can be called with one or more functions as
positional arguments. The names of these functions will be added to a list
on the class object of which they are methods.
"""
def __call__(self, f: _Tc, /, *funcObjs: Callable[..., object]) -> _Tc:
"""
Add one or more functions to the set of exposed functions.
This is a way to declare something about a class definition, similar to
L{zope.interface.implementer}. Use it like this::
magic = Expose('perform extra magic')
class Foo(Bar):
def twiddle(self, x, y):
...
def frob(self, a, b):
...
magic(twiddle, frob)
Later you can query the object::
aFoo = Foo()
magic.get(aFoo, 'twiddle')(x=1, y=2)
The call to C{get} will fail if the name it is given has not been
exposed using C{magic}.
@param funcObjs: One or more function objects which will be exposed to
the client.
@return: The first of C{funcObjs}.
"""
for fObj in itertools.chain([f], funcObjs):
exposedThrough: List[Expose] = getattr(fObj, "exposedThrough", [])
exposedThrough.append(self)
setattr(fObj, "exposedThrough", exposedThrough)
return f
_nodefault = object()
@overload
def get(self, instance: object, methodName: str) -> Callable[..., Any]:
...
@overload
def get(
self, instance: object, methodName: str, default: T
) -> Union[Callable[..., Any], T]:
...
def get(
self, instance: object, methodName: str, default: object = _nodefault
) -> object:
"""
Retrieve an exposed method with the given name from the given instance.
@raise UnexposedMethodError: Raised if C{default} is not specified and
there is no exposed method with the given name.
@return: A callable object for the named method assigned to the given
instance.
"""
method = getattr(instance, methodName, None)
exposedThrough = getattr(method, "exposedThrough", [])
if self not in exposedThrough:
if default is self._nodefault:
raise UnexposedMethodError(self, methodName)
return default
return method
def exposer(thunk: Callable[..., object]) -> Expose:
expose = Expose()
expose.__doc__ = thunk.__doc__
return expose
@exposer
def renderer() -> None:
"""
Decorate with L{renderer} to use methods as template render directives.
For example::
class Foo(Element):
@renderer
def twiddle(self, request, tag):
return tag('Hello, world.')
<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
<span t:render="twiddle" />
</div>
Will result in this final output::
<div>
<span>Hello, world.</span>
</div>
"""
@implementer(IRenderable)
class Element:
"""
Base for classes which can render part of a page.
An Element is a renderer that can be embedded in a stan document and can
hook its template (from the loader) up to render methods.
An Element might be used to encapsulate the rendering of a complex piece of
data which is to be displayed in multiple different contexts. The Element
allows the rendering logic to be easily re-used in different ways.
Element returns render methods which are registered using
L{twisted.web._element.renderer}. For example::
class Menu(Element):
@renderer
def items(self, request, tag):
....
Render methods are invoked with two arguments: first, the
L{twisted.web.http.Request} being served and second, the tag object which
"invoked" the render method.
@ivar loader: The factory which will be used to load documents to
return from C{render}.
"""
loader: Optional[ITemplateLoader] = None
def __init__(self, loader: Optional[ITemplateLoader] = None):
if loader is not None:
self.loader = loader
def lookupRenderMethod(
self, name: str
) -> Callable[[Optional[IRequest], "Tag"], "Flattenable"]:
"""
Look up and return the named render method.
"""
method = renderer.get(self, name, None)
if method is None:
raise MissingRenderMethod(self, name)
return method
def render(self, request: Optional[IRequest]) -> "Flattenable":
"""
Implement L{IRenderable} to allow one L{Element} to be embedded in
another's template or rendering output.
(This will simply load the template from the C{loader}; when used in a
template, the flattening engine will keep track of this object
separately as the object to lookup renderers on and call
L{Element.renderer} to look them up. The resulting object from this
method is not directly associated with this L{Element}.)
"""
loader = self.loader
if loader is None:
raise MissingTemplateLoader(self)
return loader.load()

View File

@@ -0,0 +1,486 @@
# -*- test-case-name: twisted.web.test.test_flatten,twisted.web.test.test_template -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Context-free flattener/serializer for rendering Python objects, possibly
complex or arbitrarily nested, as strings.
"""
from __future__ import annotations
from inspect import iscoroutine
from io import BytesIO
from sys import exc_info
from traceback import extract_tb
from types import GeneratorType
from typing import (
Any,
Callable,
Coroutine,
Generator,
List,
Mapping,
Optional,
Sequence,
Tuple,
TypeVar,
Union,
cast,
)
from twisted.internet.defer import Deferred, ensureDeferred
from twisted.python.compat import nativeString
from twisted.python.failure import Failure
from twisted.web._stan import CDATA, CharRef, Comment, Tag, slot, voidElements
from twisted.web.error import FlattenerError, UnfilledSlot, UnsupportedType
from twisted.web.iweb import IRenderable, IRequest
T = TypeVar("T")
FlattenableRecursive = Any
"""
For documentation purposes, read C{FlattenableRecursive} as L{Flattenable}.
However, since mypy doesn't support recursive type definitions (yet?),
we'll put Any in the actual definition.
"""
Flattenable = Union[
bytes,
str,
slot,
CDATA,
Comment,
Tag,
Tuple[FlattenableRecursive, ...],
List[FlattenableRecursive],
Generator[FlattenableRecursive, None, None],
CharRef,
Deferred[FlattenableRecursive],
Coroutine[Deferred[FlattenableRecursive], object, FlattenableRecursive],
IRenderable,
]
"""
Type alias containing all types that can be flattened by L{flatten()}.
"""
# The maximum number of bytes to synchronously accumulate in the flattener
# buffer before delivering them onwards.
BUFFER_SIZE = 2**16
def escapeForContent(data: Union[bytes, str]) -> bytes:
"""
Escape some character or UTF-8 byte data for inclusion in an HTML or XML
document, by replacing metacharacters (C{&<>}) with their entity
equivalents (C{&amp;&lt;&gt;}).
This is used as an input to L{_flattenElement}'s C{dataEscaper} parameter.
@param data: The string to escape.
@return: The quoted form of C{data}. If C{data} is L{str}, return a utf-8
encoded string.
"""
if isinstance(data, str):
data = data.encode("utf-8")
data = data.replace(b"&", b"&amp;").replace(b"<", b"&lt;").replace(b">", b"&gt;")
return data
def attributeEscapingDoneOutside(data: Union[bytes, str]) -> bytes:
"""
Escape some character or UTF-8 byte data for inclusion in the top level of
an attribute. L{attributeEscapingDoneOutside} actually passes the data
through unchanged, because L{writeWithAttributeEscaping} handles the
quoting of the text within attributes outside the generator returned by
L{_flattenElement}; this is used as the C{dataEscaper} argument to that
L{_flattenElement} call so that that generator does not redundantly escape
its text output.
@param data: The string to escape.
@return: The string, unchanged, except for encoding.
"""
if isinstance(data, str):
return data.encode("utf-8")
return data
def writeWithAttributeEscaping(
write: Callable[[bytes], object]
) -> Callable[[bytes], None]:
"""
Decorate a C{write} callable so that all output written is properly quoted
for inclusion within an XML attribute value.
If a L{Tag <twisted.web.template.Tag>} C{x} is flattened within the context
of the contents of another L{Tag <twisted.web.template.Tag>} C{y}, the
metacharacters (C{<>&"}) delimiting C{x} should be passed through
unchanged, but the textual content of C{x} should still be quoted, as
usual. For example: C{<y><x>&amp;</x></y>}. That is the default behavior
of L{_flattenElement} when L{escapeForContent} is passed as the
C{dataEscaper}.
However, when a L{Tag <twisted.web.template.Tag>} C{x} is flattened within
the context of an I{attribute} of another L{Tag <twisted.web.template.Tag>}
C{y}, then the metacharacters delimiting C{x} should be quoted so that it
can be parsed from the attribute's value. In the DOM itself, this is not a
valid thing to do, but given that renderers and slots may be freely moved
around in a L{twisted.web.template} template, it is a condition which may
arise in a document and must be handled in a way which produces valid
output. So, for example, you should be able to get C{<y attr="&lt;x /&gt;"
/>}. This should also be true for other XML/HTML meta-constructs such as
comments and CDATA, so if you were to serialize a L{comment
<twisted.web.template.Comment>} in an attribute you should get C{<y
attr="&lt;-- comment --&gt;" />}. Therefore in order to capture these
meta-characters, flattening is done with C{write} callable that is wrapped
with L{writeWithAttributeEscaping}.
The final case, and hopefully the much more common one as compared to
serializing L{Tag <twisted.web.template.Tag>} and arbitrary L{IRenderable}
objects within an attribute, is to serialize a simple string, and those
should be passed through for L{writeWithAttributeEscaping} to quote
without applying a second, redundant level of quoting.
@param write: A callable which will be invoked with the escaped L{bytes}.
@return: A callable that writes data with escaping.
"""
def _write(data: bytes) -> None:
write(escapeForContent(data).replace(b'"', b"&quot;"))
return _write
def escapedCDATA(data: Union[bytes, str]) -> bytes:
"""
Escape CDATA for inclusion in a document.
@param data: The string to escape.
@return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
encoded string.
"""
if isinstance(data, str):
data = data.encode("utf-8")
return data.replace(b"]]>", b"]]]]><![CDATA[>")
def escapedComment(data: Union[bytes, str]) -> bytes:
"""
Within comments the sequence C{-->} can be mistaken as the end of the comment.
To ensure consistent parsing and valid output the sequence is replaced with C{--&gt;}.
Furthermore, whitespace is added when a comment ends in a dash. This is done to break
the connection of the ending C{-} with the closing C{-->}.
@param data: The string to escape.
@return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
encoded string.
"""
if isinstance(data, str):
data = data.encode("utf-8")
data = data.replace(b"-->", b"--&gt;")
if data and data[-1:] == b"-":
data += b" "
return data
def _getSlotValue(
name: str,
slotData: Sequence[Optional[Mapping[str, Flattenable]]],
default: Optional[Flattenable] = None,
) -> Flattenable:
"""
Find the value of the named slot in the given stack of slot data.
"""
for slotFrame in reversed(slotData):
if slotFrame is not None and name in slotFrame:
return slotFrame[name]
else:
if default is not None:
return default
raise UnfilledSlot(name)
def _fork(d: Deferred[T]) -> Deferred[T]:
"""
Create a new L{Deferred} based on C{d} that will fire and fail with C{d}'s
result or error, but will not modify C{d}'s callback type.
"""
d2: Deferred[T] = Deferred(lambda _: d.cancel())
def callback(result: T) -> T:
d2.callback(result)
return result
def errback(failure: Failure) -> Failure:
d2.errback(failure)
return failure
d.addCallbacks(callback, errback)
return d2
def _flattenElement(
request: Optional[IRequest],
root: Flattenable,
write: Callable[[bytes], object],
slotData: List[Optional[Mapping[str, Flattenable]]],
renderFactory: Optional[IRenderable],
dataEscaper: Callable[[Union[bytes, str]], bytes],
# This is annotated as Generator[T, None, None] instead of Iterator[T]
# because mypy does not consider an Iterator to be an instance of
# GeneratorType.
) -> Generator[Union[Generator[Any, Any, Any], Deferred[Flattenable]], None, None]:
"""
Make C{root} slightly more flat by yielding all its immediate contents as
strings, deferreds or generators that are recursive calls to itself.
@param request: A request object which will be passed to
L{IRenderable.render}.
@param root: An object to be made flatter. This may be of type C{unicode},
L{str}, L{slot}, L{Tag <twisted.web.template.Tag>}, L{tuple}, L{list},
L{types.GeneratorType}, L{Deferred}, or an object that implements
L{IRenderable}.
@param write: A callable which will be invoked with each L{bytes} produced
by flattening C{root}.
@param slotData: A L{list} of L{dict} mapping L{str} slot names to data
with which those slots will be replaced.
@param renderFactory: If not L{None}, an object that provides
L{IRenderable}.
@param dataEscaper: A 1-argument callable which takes L{bytes} or
L{unicode} and returns L{bytes}, quoted as appropriate for the
rendering context. This is really only one of two values:
L{attributeEscapingDoneOutside} or L{escapeForContent}, depending on
whether the rendering context is within an attribute or not. See the
explanation in L{writeWithAttributeEscaping}.
@return: An iterator that eventually writes L{bytes} to C{write}.
It can yield other iterators or L{Deferred}s; if it yields another
iterator, the caller will iterate it; if it yields a L{Deferred},
the result of that L{Deferred} will be another generator, in which
case it is iterated. See L{_flattenTree} for the trampoline that
consumes said values.
"""
def keepGoing(
newRoot: Flattenable,
dataEscaper: Callable[[Union[bytes, str]], bytes] = dataEscaper,
renderFactory: Optional[IRenderable] = renderFactory,
write: Callable[[bytes], object] = write,
) -> Generator[Union[Flattenable, Deferred[Flattenable]], None, None]:
return _flattenElement(
request, newRoot, write, slotData, renderFactory, dataEscaper
)
def keepGoingAsync(result: Deferred[Flattenable]) -> Deferred[Flattenable]:
return result.addCallback(keepGoing)
if isinstance(root, (bytes, str)):
write(dataEscaper(root))
elif isinstance(root, slot):
slotValue = _getSlotValue(root.name, slotData, root.default)
yield keepGoing(slotValue)
elif isinstance(root, CDATA):
write(b"<![CDATA[")
write(escapedCDATA(root.data))
write(b"]]>")
elif isinstance(root, Comment):
write(b"<!--")
write(escapedComment(root.data))
write(b"-->")
elif isinstance(root, Tag):
slotData.append(root.slotData)
rendererName = root.render
if rendererName is not None:
if renderFactory is None:
raise ValueError(
f'Tag wants to be rendered by method "{rendererName}" '
f"but is not contained in any IRenderable"
)
rootClone = root.clone(False)
rootClone.render = None
renderMethod = renderFactory.lookupRenderMethod(rendererName)
result = renderMethod(request, rootClone)
yield keepGoing(result)
slotData.pop()
return
if not root.tagName:
yield keepGoing(root.children)
return
write(b"<")
if isinstance(root.tagName, str):
tagName = root.tagName.encode("ascii")
else:
tagName = root.tagName
write(tagName)
for k, v in root.attributes.items():
if isinstance(k, str):
k = k.encode("ascii")
write(b" " + k + b'="')
# Serialize the contents of the attribute, wrapping the results of
# that serialization so that _everything_ is quoted.
yield keepGoing(
v, attributeEscapingDoneOutside, write=writeWithAttributeEscaping(write)
)
write(b'"')
if root.children or nativeString(tagName) not in voidElements:
write(b">")
# Regardless of whether we're in an attribute or not, switch back
# to the escapeForContent dataEscaper. The contents of a tag must
# be quoted no matter what; in the top-level document, just so
# they're valid, and if they're within an attribute, they have to
# be quoted so that after applying the *un*-quoting required to re-
# parse the tag within the attribute, all the quoting is still
# correct.
yield keepGoing(root.children, escapeForContent)
write(b"</" + tagName + b">")
else:
write(b" />")
elif isinstance(root, (tuple, list, GeneratorType)):
for element in root:
yield keepGoing(element)
elif isinstance(root, CharRef):
escaped = "&#%d;" % (root.ordinal,)
write(escaped.encode("ascii"))
elif isinstance(root, Deferred):
yield keepGoingAsync(_fork(root))
elif iscoroutine(root):
yield keepGoingAsync(
Deferred.fromCoroutine(
cast(Coroutine[Deferred[Flattenable], object, Flattenable], root)
)
)
elif IRenderable.providedBy(root):
result = root.render(request)
yield keepGoing(result, renderFactory=root)
else:
raise UnsupportedType(root)
async def _flattenTree(
request: Optional[IRequest], root: Flattenable, write: Callable[[bytes], object]
) -> None:
"""
Make C{root} into an iterable of L{bytes} and L{Deferred} by doing a depth
first traversal of the tree.
@param request: A request object which will be passed to
L{IRenderable.render}.
@param root: An object to be made flatter. This may be of type C{unicode},
L{bytes}, L{slot}, L{Tag <twisted.web.template.Tag>}, L{tuple},
L{list}, L{types.GeneratorType}, L{Deferred}, or something providing
L{IRenderable}.
@param write: A callable which will be invoked with each L{bytes} produced
by flattening C{root}.
@return: A C{Deferred}-returning coroutine that resolves to C{None}.
"""
buf = []
bufSize = 0
# Accumulate some bytes up to the buffer size so that we don't annoy the
# upstream writer with a million tiny string.
def bufferedWrite(bs: bytes) -> None:
nonlocal bufSize
buf.append(bs)
bufSize += len(bs)
if bufSize >= BUFFER_SIZE:
flushBuffer()
# Deliver the buffered content to the upstream writer as a single string.
# This is how a "big enough" buffer gets delivered, how a buffer of any
# size is delivered before execution is suspended to wait for an
# asynchronous value, and how anything left in the buffer when we're
# finished is delivered.
def flushBuffer() -> None:
nonlocal bufSize
if bufSize > 0:
write(b"".join(buf))
del buf[:]
bufSize = 0
stack: List[Generator[Any, Any, Any]] = [
_flattenElement(request, root, bufferedWrite, [], None, escapeForContent)
]
while stack:
try:
element = next(stack[-1])
if isinstance(element, Deferred):
# Before suspending flattening for an unknown amount of time,
# flush whatever data we have collected so far.
flushBuffer()
element = await element
except StopIteration:
stack.pop()
except Exception as e:
roots = []
for generator in stack:
if generator.gi_frame is not None:
roots.append(generator.gi_frame.f_locals["root"])
stack.pop()
raise FlattenerError(e, roots, extract_tb(exc_info()[2]))
else:
stack.append(element)
# Flush any data that remains in the buffer before finishing.
flushBuffer()
def flatten(
request: Optional[IRequest], root: Flattenable, write: Callable[[bytes], object]
) -> Deferred[None]:
"""
Incrementally write out a string representation of C{root} using C{write}.
In order to create a string representation, C{root} will be decomposed into
simpler objects which will themselves be decomposed and so on until strings
or objects which can easily be converted to strings are encountered.
@param request: A request object which will be passed to the C{render}
method of any L{IRenderable} provider which is encountered.
@param root: An object to be made flatter. This may be of type L{str},
L{bytes}, L{slot}, L{Tag <twisted.web.template.Tag>}, L{tuple},
L{list}, L{types.GeneratorType}, L{Deferred}, or something that
provides L{IRenderable}.
@param write: A callable which will be invoked with each L{bytes} produced
by flattening C{root}.
@return: A L{Deferred} which will be called back with C{None} when C{root}
has been completely flattened into C{write} or which will be errbacked
if an unexpected exception occurs.
"""
return ensureDeferred(_flattenTree(request, root, write))
def flattenString(request: Optional[IRequest], root: Flattenable) -> Deferred[bytes]:
"""
Collate a string representation of C{root} into a single string.
This is basically gluing L{flatten} to an L{io.BytesIO} and returning
the results. See L{flatten} for the exact meanings of C{request} and
C{root}.
@return: A L{Deferred} which will be called back with a single UTF-8 encoded
string as its result when C{root} has been completely flattened or which
will be errbacked if an unexpected exception occurs.
"""
io = BytesIO()
d = flatten(request, root, io.write)
d.addCallback(lambda _: io.getvalue())
return cast(Deferred[bytes], d)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
# -*- test-case-name: twisted.web.test.test_http -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
HTTP response code definitions.
"""
_CONTINUE = 100
SWITCHING = 101
OK = 200
CREATED = 201
ACCEPTED = 202
NON_AUTHORITATIVE_INFORMATION = 203
NO_CONTENT = 204
RESET_CONTENT = 205
PARTIAL_CONTENT = 206
MULTI_STATUS = 207
MULTIPLE_CHOICE = 300
MOVED_PERMANENTLY = 301
FOUND = 302
SEE_OTHER = 303
NOT_MODIFIED = 304
USE_PROXY = 305
TEMPORARY_REDIRECT = 307
PERMANENT_REDIRECT = 308
BAD_REQUEST = 400
UNAUTHORIZED = 401
PAYMENT_REQUIRED = 402
FORBIDDEN = 403
NOT_FOUND = 404
NOT_ALLOWED = 405
NOT_ACCEPTABLE = 406
PROXY_AUTH_REQUIRED = 407
REQUEST_TIMEOUT = 408
CONFLICT = 409
GONE = 410
LENGTH_REQUIRED = 411
PRECONDITION_FAILED = 412
REQUEST_ENTITY_TOO_LARGE = 413
REQUEST_URI_TOO_LONG = 414
UNSUPPORTED_MEDIA_TYPE = 415
REQUESTED_RANGE_NOT_SATISFIABLE = 416
EXPECTATION_FAILED = 417
IM_A_TEAPOT = 418
INTERNAL_SERVER_ERROR = 500
NOT_IMPLEMENTED = 501
BAD_GATEWAY = 502
SERVICE_UNAVAILABLE = 503
GATEWAY_TIMEOUT = 504
HTTP_VERSION_NOT_SUPPORTED = 505
INSUFFICIENT_STORAGE_SPACE = 507
NOT_EXTENDED = 510
RESPONSES = {
# 100
_CONTINUE: b"Continue",
SWITCHING: b"Switching Protocols",
# 200
OK: b"OK",
CREATED: b"Created",
ACCEPTED: b"Accepted",
NON_AUTHORITATIVE_INFORMATION: b"Non-Authoritative Information",
NO_CONTENT: b"No Content",
RESET_CONTENT: b"Reset Content.",
PARTIAL_CONTENT: b"Partial Content",
MULTI_STATUS: b"Multi-Status",
# 300
MULTIPLE_CHOICE: b"Multiple Choices",
MOVED_PERMANENTLY: b"Moved Permanently",
FOUND: b"Found",
SEE_OTHER: b"See Other",
NOT_MODIFIED: b"Not Modified",
USE_PROXY: b"Use Proxy",
# 306 not defined??
TEMPORARY_REDIRECT: b"Temporary Redirect",
PERMANENT_REDIRECT: b"Permanent Redirect",
# 400
BAD_REQUEST: b"Bad Request",
UNAUTHORIZED: b"Unauthorized",
PAYMENT_REQUIRED: b"Payment Required",
FORBIDDEN: b"Forbidden",
NOT_FOUND: b"Not Found",
NOT_ALLOWED: b"Method Not Allowed",
NOT_ACCEPTABLE: b"Not Acceptable",
PROXY_AUTH_REQUIRED: b"Proxy Authentication Required",
REQUEST_TIMEOUT: b"Request Time-out",
CONFLICT: b"Conflict",
GONE: b"Gone",
LENGTH_REQUIRED: b"Length Required",
PRECONDITION_FAILED: b"Precondition Failed",
REQUEST_ENTITY_TOO_LARGE: b"Request Entity Too Large",
REQUEST_URI_TOO_LONG: b"Request-URI Too Long",
UNSUPPORTED_MEDIA_TYPE: b"Unsupported Media Type",
REQUESTED_RANGE_NOT_SATISFIABLE: b"Requested Range not satisfiable",
EXPECTATION_FAILED: b"Expectation Failed",
IM_A_TEAPOT: b"I'm a teapot",
# 500
INTERNAL_SERVER_ERROR: b"Internal Server Error",
NOT_IMPLEMENTED: b"Not Implemented",
BAD_GATEWAY: b"Bad Gateway",
SERVICE_UNAVAILABLE: b"Service Unavailable",
GATEWAY_TIMEOUT: b"Gateway Time-out",
HTTP_VERSION_NOT_SUPPORTED: b"HTTP Version not supported",
INSUFFICIENT_STORAGE_SPACE: b"Insufficient Storage Space",
NOT_EXTENDED: b"Not Extended",
}

View File

@@ -0,0 +1,360 @@
# -*- test-case-name: twisted.web.test.test_stan -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
An s-expression-like syntax for expressing xml in pure python.
Stan tags allow you to build XML documents using Python.
Stan is a DOM, or Document Object Model, implemented using basic Python types
and functions called "flatteners". A flattener is a function that knows how to
turn an object of a specific type into something that is closer to an HTML
string. Stan differs from the W3C DOM by not being as cumbersome and heavy
weight. Since the object model is built using simple python types such as lists,
strings, and dictionaries, the API is simpler and constructing a DOM less
cumbersome.
@var voidElements: the names of HTML 'U{void
elements<http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#void-elements>}';
those which can't have contents and can therefore be self-closing in the
output.
"""
from inspect import iscoroutine, isgenerator
from typing import TYPE_CHECKING, Dict, List, Optional, Union
from warnings import warn
import attr
if TYPE_CHECKING:
from twisted.web.template import Flattenable
@attr.s(unsafe_hash=False, eq=False, auto_attribs=True)
class slot:
"""
Marker for markup insertion in a template.
"""
name: str
"""
The name of this slot.
The key which must be used in L{Tag.fillSlots} to fill it.
"""
children: List["Tag"] = attr.ib(init=False, factory=list)
"""
The L{Tag} objects included in this L{slot}'s template.
"""
default: Optional["Flattenable"] = None
"""
The default contents of this slot, if it is left unfilled.
If this is L{None}, an L{UnfilledSlot} will be raised, rather than
L{None} actually being used.
"""
filename: Optional[str] = None
"""
The name of the XML file from which this tag was parsed.
If it was not parsed from an XML file, L{None}.
"""
lineNumber: Optional[int] = None
"""
The line number on which this tag was encountered in the XML file
from which it was parsed.
If it was not parsed from an XML file, L{None}.
"""
columnNumber: Optional[int] = None
"""
The column number at which this tag was encountered in the XML file
from which it was parsed.
If it was not parsed from an XML file, L{None}.
"""
@attr.s(unsafe_hash=False, eq=False, repr=False, auto_attribs=True)
class Tag:
"""
A L{Tag} represents an XML tags with a tag name, attributes, and children.
A L{Tag} can be constructed using the special L{twisted.web.template.tags}
object, or it may be constructed directly with a tag name. L{Tag}s have a
special method, C{__call__}, which makes representing trees of XML natural
using pure python syntax.
"""
tagName: Union[bytes, str]
"""
The name of the represented element.
For a tag like C{<div></div>}, this would be C{"div"}.
"""
attributes: Dict[Union[bytes, str], "Flattenable"] = attr.ib(factory=dict)
"""The attributes of the element."""
children: List["Flattenable"] = attr.ib(factory=list)
"""The contents of this C{Tag}."""
render: Optional[str] = None
"""
The name of the render method to use for this L{Tag}.
This name will be looked up at render time by the
L{twisted.web.template.Element} doing the rendering,
via L{twisted.web.template.Element.lookupRenderMethod},
to determine which method to call.
"""
filename: Optional[str] = None
"""
The name of the XML file from which this tag was parsed.
If it was not parsed from an XML file, L{None}.
"""
lineNumber: Optional[int] = None
"""
The line number on which this tag was encountered in the XML file
from which it was parsed.
If it was not parsed from an XML file, L{None}.
"""
columnNumber: Optional[int] = None
"""
The column number at which this tag was encountered in the XML file
from which it was parsed.
If it was not parsed from an XML file, L{None}.
"""
slotData: Optional[Dict[str, "Flattenable"]] = attr.ib(init=False, default=None)
"""
The data which can fill slots.
If present, a dictionary mapping slot names to renderable values.
The values in this dict might be anything that can be present as
the child of a L{Tag}: strings, lists, L{Tag}s, generators, etc.
"""
def fillSlots(self, **slots: "Flattenable") -> "Tag":
"""
Remember the slots provided at this position in the DOM.
During the rendering of children of this node, slots with names in
C{slots} will be rendered as their corresponding values.
@return: C{self}. This enables the idiom C{return tag.fillSlots(...)} in
renderers.
"""
if self.slotData is None:
self.slotData = {}
self.slotData.update(slots)
return self
def __call__(self, *children: "Flattenable", **kw: "Flattenable") -> "Tag":
"""
Add children and change attributes on this tag.
This is implemented using __call__ because it then allows the natural
syntax::
table(tr1, tr2, width="100%", height="50%", border="1")
Children may be other tag instances, strings, functions, or any other
object which has a registered flatten.
Attributes may be 'transparent' tag instances (so that
C{a(href=transparent(data="foo", render=myhrefrenderer))} works),
strings, functions, or any other object which has a registered
flattener.
If the attribute is a python keyword, such as 'class', you can add an
underscore to the name, like 'class_'.
There is one special keyword argument, 'render', which will be used as
the name of the renderer and saved as the 'render' attribute of this
instance, rather than the DOM 'render' attribute in the attributes
dictionary.
"""
self.children.extend(children)
for k, v in kw.items():
if k[-1] == "_":
k = k[:-1]
if k == "render":
if not isinstance(v, str):
raise TypeError(
f'Value for "render" attribute must be str, got {v!r}'
)
self.render = v
else:
self.attributes[k] = v
return self
def _clone(self, obj: "Flattenable", deep: bool) -> "Flattenable":
"""
Clone a C{Flattenable} object; used by L{Tag.clone}.
Note that both lists and tuples are cloned into lists.
@param obj: an object with a clone method, a list or tuple, or something
which should be immutable.
@param deep: whether to continue cloning child objects; i.e. the
contents of lists, the sub-tags within a tag.
@return: a clone of C{obj}.
"""
if hasattr(obj, "clone"):
return obj.clone(deep)
elif isinstance(obj, (list, tuple)):
return [self._clone(x, deep) for x in obj]
elif isgenerator(obj):
warn(
"Cloning a Tag which contains a generator is unsafe, "
"since the generator can be consumed only once; "
"this is deprecated since Twisted 21.7.0 and will raise "
"an exception in the future",
DeprecationWarning,
)
return obj
elif iscoroutine(obj):
warn(
"Cloning a Tag which contains a coroutine is unsafe, "
"since the coroutine can run only once; "
"this is deprecated since Twisted 21.7.0 and will raise "
"an exception in the future",
DeprecationWarning,
)
return obj
else:
return obj
def clone(self, deep: bool = True) -> "Tag":
"""
Return a clone of this tag. If deep is True, clone all of this tag's
children. Otherwise, just shallow copy the children list without copying
the children themselves.
"""
if deep:
newchildren = [self._clone(x, True) for x in self.children]
else:
newchildren = self.children[:]
newattrs = self.attributes.copy()
for key in newattrs.keys():
newattrs[key] = self._clone(newattrs[key], True)
newslotdata = None
if self.slotData:
newslotdata = self.slotData.copy()
for key in newslotdata:
newslotdata[key] = self._clone(newslotdata[key], True)
newtag = Tag(
self.tagName,
attributes=newattrs,
children=newchildren,
render=self.render,
filename=self.filename,
lineNumber=self.lineNumber,
columnNumber=self.columnNumber,
)
newtag.slotData = newslotdata
return newtag
def clear(self) -> "Tag":
"""
Clear any existing children from this tag.
"""
self.children = []
return self
def __repr__(self) -> str:
rstr = ""
if self.attributes:
rstr += ", attributes=%r" % self.attributes
if self.children:
rstr += ", children=%r" % self.children
return f"Tag({self.tagName!r}{rstr})"
voidElements = (
"img",
"br",
"hr",
"base",
"meta",
"link",
"param",
"area",
"input",
"col",
"basefont",
"isindex",
"frame",
"command",
"embed",
"keygen",
"source",
"track",
"wbs",
)
@attr.s(unsafe_hash=False, eq=False, repr=False, auto_attribs=True)
class CDATA:
"""
A C{<![CDATA[]]>} block from a template. Given a separate representation in
the DOM so that they may be round-tripped through rendering without losing
information.
"""
data: str
"""The data between "C{<![CDATA[}" and "C{]]>}"."""
def __repr__(self) -> str:
return f"CDATA({self.data!r})"
@attr.s(unsafe_hash=False, eq=False, repr=False, auto_attribs=True)
class Comment:
"""
A C{<!-- -->} comment from a template. Given a separate representation in
the DOM so that they may be round-tripped through rendering without losing
information.
"""
data: str
"""The data between "C{<!--}" and "C{-->}"."""
def __repr__(self) -> str:
return f"Comment({self.data!r})"
@attr.s(unsafe_hash=False, eq=False, repr=False, auto_attribs=True)
class CharRef:
"""
A numeric character reference. Given a separate representation in the DOM
so that non-ASCII characters may be output as pure ASCII.
@since: 12.0
"""
ordinal: int
"""The ordinal value of the unicode character to which this object refers."""
def __repr__(self) -> str:
return "CharRef(%d)" % (self.ordinal,)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
I am a simple test resource.
"""
from twisted.web import static
class Test(static.Data):
isLeaf = True
def __init__(self):
static.Data.__init__(
self,
b"""
<html>
<head><title>Twisted Web Demo</title><head>
<body>
Hello! This is a Twisted Web test page.
</body>
</html>
""",
"text/html",
)

View File

@@ -0,0 +1,390 @@
# -*- test-case-name: twisted.web.test.test_distrib -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Distributed web servers.
This is going to have to be refactored so that argument parsing is done
by each subprocess and not by the main web server (i.e. GET, POST etc.).
"""
import copy
import os
import sys
try:
import pwd
except ImportError:
pwd = None # type: ignore[assignment]
from io import BytesIO
from xml.dom.minidom import getDOMImplementation
from twisted.internet import address, reactor
from twisted.logger import Logger
from twisted.persisted import styles
from twisted.spread import pb
from twisted.spread.banana import SIZE_LIMIT
from twisted.web import http, resource, server, static, util
from twisted.web.http_headers import Headers
class _ReferenceableProducerWrapper(pb.Referenceable):
def __init__(self, producer):
self.producer = producer
def remote_resumeProducing(self):
self.producer.resumeProducing()
def remote_pauseProducing(self):
self.producer.pauseProducing()
def remote_stopProducing(self):
self.producer.stopProducing()
class Request(pb.RemoteCopy, server.Request):
"""
A request which was received by a L{ResourceSubscription} and sent via
PB to a distributed node.
"""
def setCopyableState(self, state):
"""
Initialize this L{twisted.web.distrib.Request} based on the copied
state so that it closely resembles a L{twisted.web.server.Request}.
"""
for k in "host", "client":
tup = state[k]
addrdesc = {"INET": "TCP", "UNIX": "UNIX"}[tup[0]]
addr = {
"TCP": lambda: address.IPv4Address(addrdesc, tup[1], tup[2]),
"UNIX": lambda: address.UNIXAddress(tup[1]),
}[addrdesc]()
state[k] = addr
state["requestHeaders"] = Headers(dict(state["requestHeaders"]))
pb.RemoteCopy.setCopyableState(self, state)
# Emulate the local request interface --
self.content = BytesIO(self.content_data)
self.finish = self.remote.remoteMethod("finish")
self.setHeader = self.remote.remoteMethod("setHeader")
self.addCookie = self.remote.remoteMethod("addCookie")
self.setETag = self.remote.remoteMethod("setETag")
self.setResponseCode = self.remote.remoteMethod("setResponseCode")
self.setLastModified = self.remote.remoteMethod("setLastModified")
# To avoid failing if a resource tries to write a very long string
# all at once, this one will be handled slightly differently.
self._write = self.remote.remoteMethod("write")
def write(self, bytes):
"""
Write the given bytes to the response body.
@param bytes: The bytes to write. If this is longer than 640k, it
will be split up into smaller pieces.
"""
start = 0
end = SIZE_LIMIT
while True:
self._write(bytes[start:end])
start += SIZE_LIMIT
end += SIZE_LIMIT
if start >= len(bytes):
break
def registerProducer(self, producer, streaming):
self.remote.callRemote(
"registerProducer", _ReferenceableProducerWrapper(producer), streaming
).addErrback(self.fail)
def unregisterProducer(self):
self.remote.callRemote("unregisterProducer").addErrback(self.fail)
def fail(self, failure):
self._log.failure("", failure=failure)
pb.setUnjellyableForClass(server.Request, Request)
class Issue:
_log = Logger()
def __init__(self, request):
self.request = request
def finished(self, result):
if result is not server.NOT_DONE_YET:
assert isinstance(result, str), "return value not a string"
self.request.write(result)
self.request.finish()
def failed(self, failure):
# XXX: Argh. FIXME.
failure = str(failure)
self.request.write(
resource._UnsafeErrorPage(
http.INTERNAL_SERVER_ERROR,
"Server Connection Lost",
# GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
"Connection to distributed server lost:" + util._PRE(failure),
).render(self.request)
)
self.request.finish()
self._log.info(failure)
class ResourceSubscription(resource.Resource):
isLeaf = 1
waiting = 0
_log = Logger()
def __init__(self, host, port):
resource.Resource.__init__(self)
self.host = host
self.port = port
self.pending = []
self.publisher = None
def __getstate__(self):
"""Get persistent state for this ResourceSubscription."""
# When I unserialize,
state = copy.copy(self.__dict__)
# Publisher won't be connected...
state["publisher"] = None
# I won't be making a connection
state["waiting"] = 0
# There will be no pending requests.
state["pending"] = []
return state
def connected(self, publisher):
"""I've connected to a publisher; I'll now send all my requests."""
self._log.info("connected to publisher")
publisher.broker.notifyOnDisconnect(self.booted)
self.publisher = publisher
self.waiting = 0
for request in self.pending:
self.render(request)
self.pending = []
def notConnected(self, msg):
"""I can't connect to a publisher; I'll now reply to all pending
requests.
"""
self._log.info("could not connect to distributed web service: {msg}", msg=msg)
self.waiting = 0
self.publisher = None
for request in self.pending:
request.write("Unable to connect to distributed server.")
request.finish()
self.pending = []
def booted(self):
self.notConnected("connection dropped")
def render(self, request):
"""Render this request, from my server.
This will always be asynchronous, and therefore return NOT_DONE_YET.
It spins off a request to the pb client, and either adds it to the list
of pending issues or requests it immediately, depending on if the
client is already connected.
"""
if not self.publisher:
self.pending.append(request)
if not self.waiting:
self.waiting = 1
bf = pb.PBClientFactory()
timeout = 10
if self.host == "unix":
reactor.connectUNIX(self.port, bf, timeout)
else:
reactor.connectTCP(self.host, self.port, bf, timeout)
d = bf.getRootObject()
d.addCallbacks(self.connected, self.notConnected)
else:
i = Issue(request)
self.publisher.callRemote("request", request).addCallbacks(
i.finished, i.failed
)
return server.NOT_DONE_YET
class ResourcePublisher(pb.Root, styles.Versioned):
"""
L{ResourcePublisher} exposes a remote API which can be used to respond
to request.
@ivar site: The site which will be used for resource lookup.
@type site: L{twisted.web.server.Site}
"""
_log = Logger()
def __init__(self, site):
self.site = site
persistenceVersion = 2
def upgradeToVersion2(self):
self.application.authorizer.removeIdentity("web")
del self.application.services[self.serviceName]
del self.serviceName
del self.application
del self.perspectiveName
def getPerspectiveNamed(self, name):
return self
def remote_request(self, request):
"""
Look up the resource for the given request and render it.
"""
res = self.site.getResourceFor(request)
self._log.info(request)
result = res.render(request)
if result is not server.NOT_DONE_YET:
request.write(result)
request.finish()
return server.NOT_DONE_YET
class UserDirectory(resource.Resource):
"""
A resource which lists available user resources and serves them as
children.
@ivar _pwd: An object like L{pwd} which is used to enumerate users and
their home directories.
"""
userDirName = "public_html"
userSocketName = ".twistd-web-pb"
template = """
<html>
<head>
<title>twisted.web.distrib.UserDirectory</title>
<style>
a
{
font-family: Lucida, Verdana, Helvetica, Arial, sans-serif;
color: #369;
text-decoration: none;
}
th
{
font-family: Lucida, Verdana, Helvetica, Arial, sans-serif;
font-weight: bold;
text-decoration: none;
text-align: left;
}
pre, code
{
font-family: "Courier New", Courier, monospace;
}
p, body, td, ol, ul, menu, blockquote, div
{
font-family: Lucida, Verdana, Helvetica, Arial, sans-serif;
color: #000;
}
</style>
</head>
<body>
<h1>twisted.web.distrib.UserDirectory</h1>
%(users)s
</body>
</html>
"""
def __init__(self, userDatabase=None):
resource.Resource.__init__(self)
if userDatabase is None:
userDatabase = pwd
self._pwd = userDatabase
def _users(self):
"""
Return a list of two-tuples giving links to user resources and text to
associate with those links.
"""
users = []
for user in self._pwd.getpwall():
name, passwd, uid, gid, gecos, dir, shell = user
realname = gecos.split(",")[0]
if not realname:
realname = name
if os.path.exists(os.path.join(dir, self.userDirName)):
users.append((name, realname + " (file)"))
twistdsock = os.path.join(dir, self.userSocketName)
if os.path.exists(twistdsock):
linkName = name + ".twistd"
users.append((linkName, realname + " (twistd)"))
return users
def render_GET(self, request):
"""
Render as HTML a listing of all known users with links to their
personal resources.
"""
domImpl = getDOMImplementation()
newDoc = domImpl.createDocument(None, "ul", None)
listing = newDoc.documentElement
for link, text in self._users():
linkElement = newDoc.createElement("a")
linkElement.setAttribute("href", link + "/")
textNode = newDoc.createTextNode(text)
linkElement.appendChild(textNode)
item = newDoc.createElement("li")
item.appendChild(linkElement)
listing.appendChild(item)
htmlDoc = self.template % ({"users": listing.toxml()})
return htmlDoc.encode("utf-8")
def getChild(self, name, request):
if name == b"":
return self
td = b".twistd"
if name.endswith(td):
username = name[: -len(td)]
sub = 1
else:
username = name
sub = 0
try:
# Decode using the filesystem encoding to reverse a transformation
# done in the pwd module.
(
pw_name,
pw_passwd,
pw_uid,
pw_gid,
pw_gecos,
pw_dir,
pw_shell,
) = self._pwd.getpwnam(username.decode(sys.getfilesystemencoding()))
except KeyError:
return resource._UnsafeNoResource()
if sub:
twistdsock = os.path.join(pw_dir, self.userSocketName)
rs = ResourceSubscription("unix", twistdsock)
self.putChild(name, rs)
return rs
else:
path = os.path.join(pw_dir, self.userDirName)
if not os.path.exists(path):
return resource._UnsafeNoResource()
return static.File(path)

View File

@@ -0,0 +1,313 @@
# -*- test-case-name: twisted.web.test.test_domhelpers -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
A library for performing interesting tasks with DOM objects.
This module is now deprecated.
"""
import warnings
from io import StringIO
from incremental import Version, getVersionString
from twisted.web import microdom
from twisted.web.microdom import escape, getElementsByTagName, unescape
warningString = "twisted.web.domhelpers was deprecated at {}".format(
getVersionString(Version("Twisted", 23, 10, 0))
)
warnings.warn(warningString, DeprecationWarning, stacklevel=3)
# These modules are imported here as a shortcut.
escape
getElementsByTagName
class NodeLookupError(Exception):
pass
def substitute(request, node, subs):
"""
Look through the given node's children for strings, and
attempt to do string substitution with the given parameter.
"""
for child in node.childNodes:
if hasattr(child, "nodeValue") and child.nodeValue:
child.replaceData(0, len(child.nodeValue), child.nodeValue % subs)
substitute(request, child, subs)
def _get(node, nodeId, nodeAttrs=("id", "class", "model", "pattern")):
"""
(internal) Get a node with the specified C{nodeId} as any of the C{class},
C{id} or C{pattern} attributes.
"""
if hasattr(node, "hasAttributes") and node.hasAttributes():
for nodeAttr in nodeAttrs:
if str(node.getAttribute(nodeAttr)) == nodeId:
return node
if node.hasChildNodes():
if hasattr(node.childNodes, "length"):
length = node.childNodes.length
else:
length = len(node.childNodes)
for childNum in range(length):
result = _get(node.childNodes[childNum], nodeId)
if result:
return result
def get(node, nodeId):
"""
Get a node with the specified C{nodeId} as any of the C{class},
C{id} or C{pattern} attributes. If there is no such node, raise
L{NodeLookupError}.
"""
result = _get(node, nodeId)
if result:
return result
raise NodeLookupError(nodeId)
def getIfExists(node, nodeId):
"""
Get a node with the specified C{nodeId} as any of the C{class},
C{id} or C{pattern} attributes. If there is no such node, return
L{None}.
"""
return _get(node, nodeId)
def getAndClear(node, nodeId):
"""Get a node with the specified C{nodeId} as any of the C{class},
C{id} or C{pattern} attributes. If there is no such node, raise
L{NodeLookupError}. Remove all child nodes before returning.
"""
result = get(node, nodeId)
if result:
clearNode(result)
return result
def clearNode(node):
"""
Remove all children from the given node.
"""
node.childNodes[:] = []
def locateNodes(nodeList, key, value, noNesting=1):
"""
Find subnodes in the given node where the given attribute
has the given value.
"""
returnList = []
if not isinstance(nodeList, type([])):
return locateNodes(nodeList.childNodes, key, value, noNesting)
for childNode in nodeList:
if not hasattr(childNode, "getAttribute"):
continue
if str(childNode.getAttribute(key)) == value:
returnList.append(childNode)
if noNesting:
continue
returnList.extend(locateNodes(childNode, key, value, noNesting))
return returnList
def superSetAttribute(node, key, value):
if not hasattr(node, "setAttribute"):
return
node.setAttribute(key, value)
if node.hasChildNodes():
for child in node.childNodes:
superSetAttribute(child, key, value)
def superPrependAttribute(node, key, value):
if not hasattr(node, "setAttribute"):
return
old = node.getAttribute(key)
if old:
node.setAttribute(key, value + "/" + old)
else:
node.setAttribute(key, value)
if node.hasChildNodes():
for child in node.childNodes:
superPrependAttribute(child, key, value)
def superAppendAttribute(node, key, value):
if not hasattr(node, "setAttribute"):
return
old = node.getAttribute(key)
if old:
node.setAttribute(key, old + "/" + value)
else:
node.setAttribute(key, value)
if node.hasChildNodes():
for child in node.childNodes:
superAppendAttribute(child, key, value)
def gatherTextNodes(iNode, dounescape=0, joinWith=""):
"""Visit each child node and collect its text data, if any, into a string.
For example::
>>> doc=microdom.parseString('<a>1<b>2<c>3</c>4</b></a>')
>>> gatherTextNodes(doc.documentElement)
'1234'
With dounescape=1, also convert entities back into normal characters.
@return: the gathered nodes as a single string
@rtype: str"""
gathered = []
gathered_append = gathered.append
slice = [iNode]
while len(slice) > 0:
c = slice.pop(0)
if hasattr(c, "nodeValue") and c.nodeValue is not None:
if dounescape:
val = unescape(c.nodeValue)
else:
val = c.nodeValue
gathered_append(val)
slice[:0] = c.childNodes
return joinWith.join(gathered)
class RawText(microdom.Text):
"""This is an evil and horrible speed hack. Basically, if you have a big
chunk of XML that you want to insert into the DOM, but you don't want to
incur the cost of parsing it, you can construct one of these and insert it
into the DOM. This will most certainly only work with microdom as the API
for converting nodes to xml is different in every DOM implementation.
This could be improved by making this class a Lazy parser, so if you
inserted this into the DOM and then later actually tried to mutate this
node, it would be parsed then.
"""
def writexml(
self,
writer,
indent="",
addindent="",
newl="",
strip=0,
nsprefixes=None,
namespace=None,
):
writer.write(f"{indent}{self.data}{newl}")
def findNodes(parent, matcher, accum=None):
if accum is None:
accum = []
if not parent.hasChildNodes():
return accum
for child in parent.childNodes:
# print child, child.nodeType, child.nodeName
if matcher(child):
accum.append(child)
findNodes(child, matcher, accum)
return accum
def findNodesShallowOnMatch(parent, matcher, recurseMatcher, accum=None):
if accum is None:
accum = []
if not parent.hasChildNodes():
return accum
for child in parent.childNodes:
# print child, child.nodeType, child.nodeName
if matcher(child):
accum.append(child)
if recurseMatcher(child):
findNodesShallowOnMatch(child, matcher, recurseMatcher, accum)
return accum
def findNodesShallow(parent, matcher, accum=None):
if accum is None:
accum = []
if not parent.hasChildNodes():
return accum
for child in parent.childNodes:
if matcher(child):
accum.append(child)
else:
findNodes(child, matcher, accum)
return accum
def findElementsWithAttributeShallow(parent, attribute):
"""
Return an iterable of the elements which are direct children of C{parent}
and which have the C{attribute} attribute.
"""
return findNodesShallow(
parent,
lambda n: getattr(n, "tagName", None) is not None and n.hasAttribute(attribute),
)
def findElements(parent, matcher):
"""
Return an iterable of the elements which are children of C{parent} for
which the predicate C{matcher} returns true.
"""
return findNodes(
parent,
lambda n, matcher=matcher: getattr(n, "tagName", None) is not None
and matcher(n),
)
def findElementsWithAttribute(parent, attribute, value=None):
if value:
return findElements(
parent,
lambda n, attribute=attribute, value=value: n.hasAttribute(attribute)
and n.getAttribute(attribute) == value,
)
else:
return findElements(
parent, lambda n, attribute=attribute: n.hasAttribute(attribute)
)
def findNodesNamed(parent, name):
return findNodes(parent, lambda n, name=name: n.nodeName == name)
def writeNodeData(node, oldio):
for subnode in node.childNodes:
if hasattr(subnode, "data"):
oldio.write("" + subnode.data)
else:
writeNodeData(subnode, oldio)
def getNodeText(node):
oldio = StringIO()
writeNodeData(node, oldio)
return oldio.getvalue()
def getParents(node):
l = []
while node:
l.append(node)
node = node.parentNode
return l
def namedChildren(parent, nodeName):
"""namedChildren(parent, nodeName) -> children (not descendants) of parent
that have tagName == nodeName
"""
return [n for n in parent.childNodes if getattr(n, "tagName", "") == nodeName]

View File

@@ -0,0 +1,442 @@
# -*- test-case-name: twisted.web.test.test_error -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Exception definitions for L{twisted.web}.
"""
__all__ = [
"Error",
"PageRedirect",
"InfiniteRedirection",
"RenderError",
"MissingRenderMethod",
"MissingTemplateLoader",
"UnexposedMethodError",
"UnfilledSlot",
"UnsupportedType",
"FlattenerError",
"RedirectWithNoLocation",
]
from collections.abc import Sequence
from typing import Optional, Union, cast
from twisted.python.compat import nativeString
from twisted.web._responses import RESPONSES
def _codeToMessage(code: Union[int, bytes]) -> Optional[bytes]:
"""
Returns the response message corresponding to an HTTP code, or None
if the code is unknown or unrecognized.
@param code: HTTP status code, for example C{http.NOT_FOUND}.
@return: A string message or none
"""
try:
return RESPONSES.get(int(code))
except (ValueError, AttributeError):
return None
class Error(Exception):
"""
A basic HTTP error.
@ivar status: Refers to an HTTP status code, for example C{http.NOT_FOUND}.
@param message: A short error message, for example "NOT FOUND".
@ivar response: A complete HTML document for an error page.
"""
status: bytes
message: Optional[bytes]
response: Optional[bytes]
def __init__(
self,
code: Union[int, bytes],
message: Optional[bytes] = None,
response: Optional[bytes] = None,
) -> None:
"""
Initializes a basic exception.
@type code: L{bytes} or L{int}
@param code: Refers to an HTTP status code (for example, 200) either as
an integer or a bytestring representing such. If no C{message} is
given, C{code} is mapped to a descriptive bytestring that is used
instead.
@type message: L{bytes}
@param message: A short error message, for example C{b"NOT FOUND"}.
@type response: L{bytes}
@param response: A complete HTML document for an error page.
"""
message = message or _codeToMessage(code)
Exception.__init__(self, code, message, response)
if isinstance(code, int):
# If we're given an int, convert it to a bytestring
# downloadPage gives a bytes, Agent gives an int, and it worked by
# accident previously, so just make it keep working.
code = b"%d" % (code,)
elif len(code) != 3 or not code.isdigit():
# Status codes must be 3 digits. See
# https://httpwg.org/specs/rfc9110.html#status.code.extensibility
raise ValueError(f"Not a valid HTTP status code: {code!r}")
self.status = code
self.message = message
self.response = response
def __str__(self) -> str:
s = self.status
if self.message:
s += b" " + self.message
return nativeString(s)
class PageRedirect(Error):
"""
A request resulted in an HTTP redirect.
@ivar location: The location of the redirect which was not followed.
"""
location: Optional[bytes]
def __init__(
self,
code: Union[int, bytes],
message: Optional[bytes] = None,
response: Optional[bytes] = None,
location: Optional[bytes] = None,
) -> None:
"""
Initializes a page redirect exception.
@type code: L{bytes}
@param code: Refers to an HTTP status code, for example
C{http.NOT_FOUND}. If no C{message} is given, C{code} is mapped to a
descriptive string that is used instead.
@type message: L{bytes}
@param message: A short error message, for example C{b"NOT FOUND"}.
@type response: L{bytes}
@param response: A complete HTML document for an error page.
@type location: L{bytes}
@param location: The location response-header field value. It is an
absolute URI used to redirect the receiver to a location other than
the Request-URI so the request can be completed.
"""
Error.__init__(self, code, message, response)
if self.message and location:
self.message = self.message + b" to " + location
self.location = location
class InfiniteRedirection(Error):
"""
HTTP redirection is occurring endlessly.
@ivar location: The first URL in the series of redirections which was
not followed.
"""
location: Optional[bytes]
def __init__(
self,
code: Union[int, bytes],
message: Optional[bytes] = None,
response: Optional[bytes] = None,
location: Optional[bytes] = None,
) -> None:
"""
Initializes an infinite redirection exception.
@param code: Refers to an HTTP status code, for example
C{http.NOT_FOUND}. If no C{message} is given, C{code} is mapped to a
descriptive string that is used instead.
@param message: A short error message, for example C{b"NOT FOUND"}.
@param response: A complete HTML document for an error page.
@param location: The location response-header field value. It is an
absolute URI used to redirect the receiver to a location other than
the Request-URI so the request can be completed.
"""
Error.__init__(self, code, message, response)
if self.message and location:
self.message = self.message + b" to " + location
self.location = location
class RedirectWithNoLocation(Error):
"""
Exception passed to L{ResponseFailed} if we got a redirect without a
C{Location} header field.
@type uri: L{bytes}
@ivar uri: The URI which failed to give a proper location header
field.
@since: 11.1
"""
message: bytes
uri: bytes
def __init__(self, code: Union[bytes, int], message: bytes, uri: bytes) -> None:
"""
Initializes a page redirect exception when no location is given.
@type code: L{bytes}
@param code: Refers to an HTTP status code, for example
C{http.NOT_FOUND}. If no C{message} is given, C{code} is mapped to
a descriptive string that is used instead.
@type message: L{bytes}
@param message: A short error message.
@type uri: L{bytes}
@param uri: The URI which failed to give a proper location header
field.
"""
Error.__init__(self, code, message)
self.message = self.message + b" to " + uri
self.uri = uri
class UnsupportedMethod(Exception):
"""
Raised by a resource when faced with a strange request method.
RFC 2616 (HTTP 1.1) gives us two choices when faced with this situation:
If the type of request is known to us, but not allowed for the requested
resource, respond with NOT_ALLOWED. Otherwise, if the request is something
we don't know how to deal with in any case, respond with NOT_IMPLEMENTED.
When this exception is raised by a Resource's render method, the server
will make the appropriate response.
This exception's first argument MUST be a sequence of the methods the
resource *does* support.
"""
allowedMethods = ()
def __init__(self, allowedMethods, *args):
Exception.__init__(self, allowedMethods, *args)
self.allowedMethods = allowedMethods
if not isinstance(allowedMethods, Sequence):
raise TypeError(
"First argument must be a sequence of supported methods, "
"but my first argument is not a sequence."
)
def __str__(self) -> str:
return f"Expected one of {self.allowedMethods!r}"
class SchemeNotSupported(Exception):
"""
The scheme of a URI was not one of the supported values.
"""
class RenderError(Exception):
"""
Base exception class for all errors which can occur during template
rendering.
"""
class MissingRenderMethod(RenderError):
"""
Tried to use a render method which does not exist.
@ivar element: The element which did not have the render method.
@ivar renderName: The name of the renderer which could not be found.
"""
def __init__(self, element, renderName):
RenderError.__init__(self, element, renderName)
self.element = element
self.renderName = renderName
def __repr__(self) -> str:
return "{!r}: {!r} had no render method named {!r}".format(
self.__class__.__name__,
self.element,
self.renderName,
)
class MissingTemplateLoader(RenderError):
"""
L{MissingTemplateLoader} is raised when trying to render an Element without
a template loader, i.e. a C{loader} attribute.
@ivar element: The Element which did not have a document factory.
"""
def __init__(self, element):
RenderError.__init__(self, element)
self.element = element
def __repr__(self) -> str:
return f"{self.__class__.__name__!r}: {self.element!r} had no loader"
class UnexposedMethodError(Exception):
"""
Raised on any attempt to get a method which has not been exposed.
"""
class UnfilledSlot(Exception):
"""
During flattening, a slot with no associated data was encountered.
"""
class UnsupportedType(Exception):
"""
During flattening, an object of a type which cannot be flattened was
encountered.
"""
class ExcessiveBufferingError(Exception):
"""
The HTTP/2 protocol has been forced to buffer an excessive amount of
outbound data, and has therefore closed the connection and dropped all
outbound data.
"""
class FlattenerError(Exception):
"""
An error occurred while flattening an object.
@ivar _roots: A list of the objects on the flattener's stack at the time
the unflattenable object was encountered. The first element is least
deeply nested object and the last element is the most deeply nested.
"""
def __init__(self, exception, roots, traceback):
self._exception = exception
self._roots = roots
self._traceback = traceback
Exception.__init__(self, exception, roots, traceback)
def _formatRoot(self, obj):
"""
Convert an object from C{self._roots} to a string suitable for
inclusion in a render-traceback (like a normal Python traceback, but
can include "frame" source locations which are not in Python source
files).
@param obj: Any object which can be a render step I{root}.
Typically, L{Tag}s, strings, and other simple Python types.
@return: A string representation of C{obj}.
@rtype: L{str}
"""
# There's a circular dependency between this class and 'Tag', although
# only for an isinstance() check.
from twisted.web.template import Tag
if isinstance(obj, (bytes, str)):
# It's somewhat unlikely that there will ever be a str in the roots
# list. However, something like a MemoryError during a str.replace
# call (eg, replacing " with &quot;) could possibly cause this.
# Likewise, UTF-8 encoding a unicode string to a byte string might
# fail like this.
if len(obj) > 40:
if isinstance(obj, str):
ellipsis = "<...>"
else:
ellipsis = b"<...>"
return ascii(obj[:20] + ellipsis + obj[-20:])
else:
return ascii(obj)
elif isinstance(obj, Tag):
if obj.filename is None:
return "Tag <" + obj.tagName + ">"
else:
return 'File "%s", line %d, column %d, in "%s"' % (
obj.filename,
obj.lineNumber,
obj.columnNumber,
obj.tagName,
)
else:
return ascii(obj)
def __repr__(self) -> str:
"""
Present a string representation which includes a template traceback, so
we can tell where this error occurred in the template, as well as in
Python.
"""
# Avoid importing things unnecessarily until we actually need them;
# since this is an 'error' module we should be extra paranoid about
# that.
from traceback import format_list
if self._roots:
roots = (
" " + "\n ".join([self._formatRoot(r) for r in self._roots]) + "\n"
)
else:
roots = ""
if self._traceback:
traceback = (
"\n".join(
[
line
for entry in format_list(self._traceback)
for line in entry.splitlines()
]
)
+ "\n"
)
else:
traceback = ""
return cast(
str,
(
"Exception while flattening:\n"
+ roots
+ traceback
+ self._exception.__class__.__name__
+ ": "
+ str(self._exception)
+ "\n"
),
)
def __str__(self) -> str:
return repr(self)
class UnsupportedSpecialHeader(Exception):
"""
A HTTP/2 request was received that contained a HTTP/2 pseudo-header field
that is not recognised by Twisted.
"""

View File

@@ -0,0 +1,21 @@
# -*- test-case-name: twisted.web.test.test_httpauth -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Resource traversal integration with L{twisted.cred} to allow for
authentication and authorization of HTTP requests.
"""
from twisted.web._auth.basic import BasicCredentialFactory
from twisted.web._auth.digest import DigestCredentialFactory
# Expose HTTP authentication classes here.
from twisted.web._auth.wrapper import HTTPAuthSessionWrapper
__all__ = [
"HTTPAuthSessionWrapper",
"BasicCredentialFactory",
"DigestCredentialFactory",
]

View File

@@ -0,0 +1,56 @@
# -*- test-case-name: twisted.web.test.test_html -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""I hold HTML generation helpers.
"""
from html import escape
from io import StringIO
from incremental import Version
from twisted.python import log
from twisted.python.deprecate import deprecated
@deprecated(Version("Twisted", 15, 3, 0), replacement="twisted.web.template")
def PRE(text):
"Wrap <pre> tags around some text and HTML-escape it."
return "<pre>" + escape(text) + "</pre>"
@deprecated(Version("Twisted", 15, 3, 0), replacement="twisted.web.template")
def UL(lst):
io = StringIO()
io.write("<ul>\n")
for el in lst:
io.write("<li> %s</li>\n" % el)
io.write("</ul>")
return io.getvalue()
@deprecated(Version("Twisted", 15, 3, 0), replacement="twisted.web.template")
def linkList(lst):
io = StringIO()
io.write("<ul>\n")
for hr, el in lst:
io.write(f'<li> <a href="{hr}">{el}</a></li>\n')
io.write("</ul>")
return io.getvalue()
@deprecated(Version("Twisted", 15, 3, 0), replacement="twisted.web.template")
def output(func, *args, **kw):
"""output(func, *args, **kw) -> html string
Either return the result of a function (which presumably returns an
HTML-legal string) or a sparse HTMLized error message and a message
in the server log.
"""
try:
return func(*args, **kw)
except BaseException:
log.msg(f"Error calling {func!r}:")
log.err()
return PRE("An error occurred.")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,284 @@
# -*- test-case-name: twisted.web.test.test_http_headers -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
An API for storing HTTP header names and values.
"""
from typing import (
AnyStr,
ClassVar,
Dict,
Iterator,
List,
Mapping,
Optional,
Sequence,
Tuple,
TypeVar,
Union,
overload,
)
from twisted.python.compat import cmp, comparable
from twisted.web._abnf import _istoken
class InvalidHeaderName(ValueError):
"""
HTTP header names must be tokens, per RFC 9110 section 5.1.
"""
_T = TypeVar("_T")
def _sanitizeLinearWhitespace(headerComponent: bytes) -> bytes:
r"""
Replace linear whitespace (C{\n}, C{\r\n}, C{\r}) in a header
value with a single space.
@param headerComponent: The header value to sanitize.
@return: The sanitized header value.
"""
return b" ".join(headerComponent.splitlines())
@comparable
class Headers:
"""
Stores HTTP headers in a key and multiple value format.
When passed L{str}, header names (e.g. 'Content-Type')
are encoded using ISO-8859-1 and header values (e.g.
'text/html;charset=utf-8') are encoded using UTF-8. Some methods that return
values will return them in the same type as the name given.
If the header keys or values cannot be encoded or decoded using the rules
above, using just L{bytes} arguments to the methods of this class will
ensure no decoding or encoding is done, and L{Headers} will treat the keys
and values as opaque byte strings.
@ivar _rawHeaders: A L{dict} mapping header names as L{bytes} to L{list}s of
header values as L{bytes}.
"""
__slots__ = ["_rawHeaders"]
def __init__(
self,
rawHeaders: Optional[Mapping[AnyStr, Sequence[AnyStr]]] = None,
) -> None:
self._rawHeaders: Dict[bytes, List[bytes]] = {}
if rawHeaders is not None:
for name, values in rawHeaders.items():
self.setRawHeaders(name, values)
def __repr__(self) -> str:
"""
Return a string fully describing the headers set on this object.
"""
return "{}({!r})".format(
self.__class__.__name__,
self._rawHeaders,
)
def __cmp__(self, other):
"""
Define L{Headers} instances as being equal to each other if they have
the same raw headers.
"""
if isinstance(other, Headers):
return cmp(
sorted(self._rawHeaders.items()), sorted(other._rawHeaders.items())
)
return NotImplemented
def copy(self):
"""
Return a copy of itself with the same headers set.
@return: A new L{Headers}
"""
return self.__class__(self._rawHeaders)
def hasHeader(self, name: AnyStr) -> bool:
"""
Check for the existence of a given header.
@param name: The name of the HTTP header to check for.
@return: C{True} if the header exists, otherwise C{False}.
"""
return _nameEncoder.encode(name) in self._rawHeaders
def removeHeader(self, name: AnyStr) -> None:
"""
Remove the named header from this header object.
@param name: The name of the HTTP header to remove.
@return: L{None}
"""
self._rawHeaders.pop(_nameEncoder.encode(name), None)
def setRawHeaders(
self, name: Union[str, bytes], values: Sequence[Union[str, bytes]]
) -> None:
"""
Sets the raw representation of the given header.
@param name: The name of the HTTP header to set the values for.
@param values: A list of strings each one being a header value of
the given name.
@raise TypeError: Raised if C{values} is not a sequence of L{bytes}
or L{str}, or if C{name} is not L{bytes} or L{str}.
@return: L{None}
"""
_name = _nameEncoder.encode(name)
encodedValues: List[bytes] = []
for v in values:
if isinstance(v, str):
_v = v.encode("utf8")
else:
_v = v
encodedValues.append(_sanitizeLinearWhitespace(_v))
self._rawHeaders[_name] = encodedValues
def addRawHeader(self, name: Union[str, bytes], value: Union[str, bytes]) -> None:
"""
Add a new raw value for the given header.
@param name: The name of the header for which to set the value.
@param value: The value to set for the named header.
"""
self._rawHeaders.setdefault(_nameEncoder.encode(name), []).append(
_sanitizeLinearWhitespace(
value.encode("utf8") if isinstance(value, str) else value
)
)
@overload
def getRawHeaders(self, name: AnyStr) -> Optional[Sequence[AnyStr]]:
...
@overload
def getRawHeaders(self, name: AnyStr, default: _T) -> Union[Sequence[AnyStr], _T]:
...
def getRawHeaders(
self, name: AnyStr, default: Optional[_T] = None
) -> Union[Sequence[AnyStr], Optional[_T]]:
"""
Returns a sequence of headers matching the given name as the raw string
given.
@param name: The name of the HTTP header to get the values of.
@param default: The value to return if no header with the given C{name}
exists.
@return: If the named header is present, a sequence of its
values. Otherwise, C{default}.
"""
encodedName = _nameEncoder.encode(name)
values = self._rawHeaders.get(encodedName, [])
if not values:
return default
if isinstance(name, str):
return [v.decode("utf8") for v in values]
return values
def getAllRawHeaders(self) -> Iterator[Tuple[bytes, Sequence[bytes]]]:
"""
Return an iterator of key, value pairs of all headers contained in this
object, as L{bytes}. The keys are capitalized in canonical
capitalization.
"""
return iter(self._rawHeaders.items())
class _NameEncoder:
"""
C{_NameEncoder} converts HTTP header names to L{bytes} and canonicalizies
their capitalization.
@cvar _caseMappings: A L{dict} that maps conventionally-capitalized
header names to their canonicalized representation, for headers with
unconventional capitalization.
@cvar _canonicalHeaderCache: A L{dict} that maps header names to their
canonicalized representation.
"""
__slots__ = ("_canonicalHeaderCache",)
_canonicalHeaderCache: Dict[Union[bytes, str], bytes]
_caseMappings: ClassVar[Dict[bytes, bytes]] = {
b"Content-Md5": b"Content-MD5",
b"Dnt": b"DNT",
b"Etag": b"ETag",
b"P3p": b"P3P",
b"Te": b"TE",
b"Www-Authenticate": b"WWW-Authenticate",
b"X-Xss-Protection": b"X-XSS-Protection",
}
_MAX_CACHED_HEADERS: ClassVar[int] = 10_000
def __init__(self):
self._canonicalHeaderCache = {}
def encode(self, name: Union[str, bytes]) -> bytes:
"""
Encode the name of a header (eg 'Content-Type') to an ISO-8859-1
bytestring if required. It will be canonicalized to Http-Header-Case.
@raises InvalidHeaderName:
If the header name contains invalid characters like whitespace
or NUL.
@param name: An HTTP header name
@return: C{name}, encoded if required, in Header-Case
"""
if canonicalName := self._canonicalHeaderCache.get(name):
return canonicalName
bytes_name = name.encode("iso-8859-1") if isinstance(name, str) else name
if not _istoken(bytes_name):
raise InvalidHeaderName(bytes_name)
result = b"-".join([word.capitalize() for word in bytes_name.split(b"-")])
# Some headers have special capitalization:
if result in self._caseMappings:
result = self._caseMappings[result]
# In general, we should only see a very small number of header
# variations in the real world, so caching them is fine. However, an
# attacker could generate infinite header variations to fill up RAM, so
# we cap how many we cache. The performance degradation from lack of
# caching won't be that bad, and legit traffic won't hit it.
if len(self._canonicalHeaderCache) < self._MAX_CACHED_HEADERS:
self._canonicalHeaderCache[name] = result
return result
_nameEncoder = _NameEncoder()
"""
The global name encoder.
"""
__all__ = ["Headers"]

View File

@@ -0,0 +1,831 @@
# -*- test-case-name: twisted.web.test -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Interface definitions for L{twisted.web}.
@var UNKNOWN_LENGTH: An opaque object which may be used as the value of
L{IBodyProducer.length} to indicate that the length of the entity
body is not known in advance.
"""
from typing import TYPE_CHECKING, Callable, List, Optional
from zope.interface import Attribute, Interface
from twisted.cred.credentials import IUsernameDigestHash
from twisted.internet.defer import Deferred
from twisted.internet.interfaces import IPushProducer
from twisted.web.http_headers import Headers
if TYPE_CHECKING:
from twisted.web.template import Flattenable, Tag
class IRequest(Interface):
"""
An HTTP request.
@since: 9.0
"""
method = Attribute("A L{bytes} giving the HTTP method that was used.")
uri = Attribute(
"A L{bytes} giving the full encoded URI which was requested (including"
" query arguments)."
)
path = Attribute(
"A L{bytes} giving the encoded query path of the request URI (not "
"including query arguments)."
)
args = Attribute(
"A mapping of decoded query argument names as L{bytes} to "
"corresponding query argument values as L{list}s of L{bytes}. "
"For example, for a URI with C{foo=bar&foo=baz&quux=spam} "
"for its query part, C{args} will be C{{b'foo': [b'bar', b'baz'], "
"b'quux': [b'spam']}}."
)
prepath = Attribute(
"The URL path segments which have been processed during resource "
"traversal, as a list of L{bytes}."
)
postpath = Attribute(
"The URL path segments which have not (yet) been processed "
"during resource traversal, as a list of L{bytes}."
)
requestHeaders = Attribute(
"A L{http_headers.Headers} instance giving all received HTTP request "
"headers."
)
content = Attribute(
"A file-like object giving the request body. This may be a file on "
"disk, an L{io.BytesIO}, or some other type. The implementation is "
"free to decide on a per-request basis."
)
responseHeaders = Attribute(
"A L{http_headers.Headers} instance holding all HTTP response "
"headers to be sent."
)
def getHeader(key):
"""
Get an HTTP request header.
@type key: L{bytes} or L{str}
@param key: The name of the header to get the value of.
@rtype: L{bytes} or L{str} or L{None}
@return: The value of the specified header, or L{None} if that header
was not present in the request. The string type of the result
matches the type of C{key}.
"""
def getCookie(key):
"""
Get a cookie that was sent from the network.
@type key: L{bytes}
@param key: The name of the cookie to get.
@rtype: L{bytes} or L{None}
@returns: The value of the specified cookie, or L{None} if that cookie
was not present in the request.
"""
def getAllHeaders():
"""
Return dictionary mapping the names of all received headers to the last
value received for each.
Since this method does not return all header information,
C{requestHeaders.getAllRawHeaders()} may be preferred.
"""
def getRequestHostname():
"""
Get the hostname that the HTTP client passed in to the request.
This will either use the C{Host:} header (if it is available; which,
for a spec-compliant request, it will be) or the IP address of the host
we are listening on if the header is unavailable.
@note: This is the I{host portion} of the requested resource, which
means that:
1. it might be an IPv4 or IPv6 address, not just a DNS host
name,
2. there's no guarantee it's even a I{valid} host name or IP
address, since the C{Host:} header may be malformed,
3. it does not include the port number.
@returns: the requested hostname
@rtype: L{bytes}
"""
def getHost():
"""
Get my originally requesting transport's host.
@return: An L{IAddress<twisted.internet.interfaces.IAddress>}.
"""
def getClientAddress():
"""
Return the address of the client who submitted this request.
The address may not be a network address. Callers must check
its type before using it.
@since: 18.4
@return: the client's address.
@rtype: an L{IAddress} provider.
"""
def getClientIP():
"""
Return the IP address of the client who submitted this request.
This method is B{deprecated}. See L{getClientAddress} instead.
@returns: the client IP address or L{None} if the request was submitted
over a transport where IP addresses do not make sense.
@rtype: L{str} or L{None}
"""
def getUser():
"""
Return the HTTP user sent with this request, if any.
If no user was supplied, return the empty string.
@returns: the HTTP user, if any
@rtype: L{str}
"""
def getPassword():
"""
Return the HTTP password sent with this request, if any.
If no password was supplied, return the empty string.
@returns: the HTTP password, if any
@rtype: L{str}
"""
def isSecure():
"""
Return True if this request is using a secure transport.
Normally this method returns True if this request's HTTPChannel
instance is using a transport that implements ISSLTransport.
This will also return True if setHost() has been called
with ssl=True.
@returns: True if this request is secure
@rtype: C{bool}
"""
def getSession(sessionInterface=None):
"""
Look up the session associated with this request or create a new one if
there is not one.
@return: The L{Session} instance identified by the session cookie in
the request, or the C{sessionInterface} component of that session
if C{sessionInterface} is specified.
"""
def URLPath():
"""
@return: A L{URLPath<twisted.python.urlpath.URLPath>} instance
which identifies the URL for which this request is.
"""
def prePathURL():
"""
At any time during resource traversal or resource rendering,
returns an absolute URL to the most nested resource which has
yet been reached.
@see: {twisted.web.server.Request.prepath}
@return: An absolute URL.
@rtype: L{bytes}
"""
def rememberRootURL():
"""
Remember the currently-processed part of the URL for later
recalling.
"""
def getRootURL():
"""
Get a previously-remembered URL.
@return: An absolute URL.
@rtype: L{bytes}
"""
# Methods for outgoing response
def finish():
"""
Indicate that the response to this request is complete.
"""
def write(data):
"""
Write some data to the body of the response to this request. Response
headers are written the first time this method is called, after which
new response headers may not be added.
@param data: Bytes of the response body.
@type data: L{bytes}
"""
def addCookie(
k,
v,
expires=None,
domain=None,
path=None,
max_age=None,
comment=None,
secure=None,
):
"""
Set an outgoing HTTP cookie.
In general, you should consider using sessions instead of cookies, see
L{twisted.web.server.Request.getSession} and the
L{twisted.web.server.Session} class for details.
"""
def setResponseCode(code, message=None):
"""
Set the HTTP response code.
@type code: L{int}
@type message: L{bytes}
"""
def setHeader(k, v):
"""
Set an HTTP response header. Overrides any previously set values for
this header.
@type k: L{bytes} or L{str}
@param k: The name of the header for which to set the value.
@type v: L{bytes} or L{str}
@param v: The value to set for the named header. A L{str} will be
UTF-8 encoded, which may not interoperable with other
implementations. Avoid passing non-ASCII characters if possible.
"""
def redirect(url):
"""
Utility function that does a redirect.
The request should have finish() called after this.
"""
def setLastModified(when):
"""
Set the C{Last-Modified} time for the response to this request.
If I am called more than once, I ignore attempts to set Last-Modified
earlier, only replacing the Last-Modified time if it is to a later
value.
If I am a conditional request, I may modify my response code to
L{NOT_MODIFIED<http.NOT_MODIFIED>} if appropriate for the time given.
@param when: The last time the resource being returned was modified, in
seconds since the epoch.
@type when: L{int} or L{float}
@return: If I am a C{If-Modified-Since} conditional request and the time
given is not newer than the condition, I return
L{CACHED<http.CACHED>} to indicate that you should write no body.
Otherwise, I return a false value.
"""
def setETag(etag):
"""
Set an C{entity tag} for the outgoing response.
That's "entity tag" as in the HTTP/1.1 I{ETag} header, "used for
comparing two or more entities from the same requested resource."
If I am a conditional request, I may modify my response code to
L{NOT_MODIFIED<http.NOT_MODIFIED>} or
L{PRECONDITION_FAILED<http.PRECONDITION_FAILED>}, if appropriate for the
tag given.
@param etag: The entity tag for the resource being returned.
@type etag: L{str}
@return: If I am a C{If-None-Match} conditional request and the tag
matches one in the request, I return L{CACHED<http.CACHED>} to
indicate that you should write no body. Otherwise, I return a
false value.
"""
def setHost(host, port, ssl=0):
"""
Change the host and port the request thinks it's using.
This method is useful for working with reverse HTTP proxies (e.g. both
Squid and Apache's mod_proxy can do this), when the address the HTTP
client is using is different than the one we're listening on.
For example, Apache may be listening on https://www.example.com, and
then forwarding requests to http://localhost:8080, but we don't want
HTML produced by Twisted to say 'http://localhost:8080', they should
say 'https://www.example.com', so we do::
request.setHost('www.example.com', 443, ssl=1)
"""
class INonQueuedRequestFactory(Interface):
"""
A factory of L{IRequest} objects that does not take a ``queued`` parameter.
"""
def __call__(channel):
"""
Create an L{IRequest} that is operating on the given channel. There
must only be one L{IRequest} object processing at any given time on a
channel.
@param channel: A L{twisted.web.http.HTTPChannel} object.
@type channel: L{twisted.web.http.HTTPChannel}
@return: A request object.
@rtype: L{IRequest}
"""
class IAccessLogFormatter(Interface):
"""
An object which can represent an HTTP request as a line of text for
inclusion in an access log file.
"""
def __call__(timestamp, request):
"""
Generate a line for the access log.
@param timestamp: The time at which the request was completed in the
standard format for access logs.
@type timestamp: L{unicode}
@param request: The request object about which to log.
@type request: L{twisted.web.server.Request}
@return: One line describing the request without a trailing newline.
@rtype: L{unicode}
"""
class ICredentialFactory(Interface):
"""
A credential factory defines a way to generate a particular kind of
authentication challenge and a way to interpret the responses to these
challenges. It creates
L{ICredentials<twisted.cred.credentials.ICredentials>} providers from
responses. These objects will be used with L{twisted.cred} to authenticate
an authorize requests.
"""
scheme = Attribute(
"A L{str} giving the name of the authentication scheme with which "
"this factory is associated. For example, C{'basic'} or C{'digest'}."
)
def getChallenge(request):
"""
Generate a new challenge to be sent to a client.
@type request: L{twisted.web.http.Request}
@param request: The request the response to which this challenge will
be included.
@rtype: L{dict}
@return: A mapping from L{str} challenge fields to associated L{str}
values.
"""
def decode(response, request):
"""
Create a credentials object from the given response.
@type response: L{str}
@param response: scheme specific response string
@type request: L{twisted.web.http.Request}
@param request: The request being processed (from which the response
was taken).
@raise twisted.cred.error.LoginFailed: If the response is invalid.
@rtype: L{twisted.cred.credentials.ICredentials} provider
@return: The credentials represented by the given response.
"""
class IBodyProducer(IPushProducer):
"""
Objects which provide L{IBodyProducer} write bytes to an object which
provides L{IConsumer<twisted.internet.interfaces.IConsumer>} by calling its
C{write} method repeatedly.
L{IBodyProducer} providers may start producing as soon as they have an
L{IConsumer<twisted.internet.interfaces.IConsumer>} provider. That is, they
should not wait for a C{resumeProducing} call to begin writing data.
L{IConsumer.unregisterProducer<twisted.internet.interfaces.IConsumer.unregisterProducer>}
must not be called. Instead, the
L{Deferred<twisted.internet.defer.Deferred>} returned from C{startProducing}
must be fired when all bytes have been written.
L{IConsumer.write<twisted.internet.interfaces.IConsumer.write>} may
synchronously invoke any of C{pauseProducing}, C{resumeProducing}, or
C{stopProducing}. These methods must be implemented with this in mind.
@since: 9.0
"""
# Despite the restrictions above and the additional requirements of
# stopProducing documented below, this interface still needs to be an
# IPushProducer subclass. Providers of it will be passed to IConsumer
# providers which only know about IPushProducer and IPullProducer, not
# about this interface. This interface needs to remain close enough to one
# of those interfaces for consumers to work with it.
length = Attribute(
"""
C{length} is a L{int} indicating how many bytes in total this
L{IBodyProducer} will write to the consumer or L{UNKNOWN_LENGTH}
if this is not known in advance.
"""
)
def startProducing(consumer):
"""
Start producing to the given
L{IConsumer<twisted.internet.interfaces.IConsumer>} provider.
@return: A L{Deferred<twisted.internet.defer.Deferred>} which stops
production of data when L{Deferred.cancel} is called, and which
fires with L{None} when all bytes have been produced or with a
L{Failure<twisted.python.failure.Failure>} if there is any problem
before all bytes have been produced.
"""
def stopProducing():
"""
In addition to the standard behavior of
L{IProducer.stopProducing<twisted.internet.interfaces.IProducer.stopProducing>}
(stop producing data), make sure the
L{Deferred<twisted.internet.defer.Deferred>} returned by
C{startProducing} is never fired.
"""
class IRenderable(Interface):
"""
An L{IRenderable} is an object that may be rendered by the
L{twisted.web.template} templating system.
"""
def lookupRenderMethod(
name: str,
) -> Callable[[Optional[IRequest], "Tag"], "Flattenable"]:
"""
Look up and return the render method associated with the given name.
@param name: The value of a render directive encountered in the
document returned by a call to L{IRenderable.render}.
@return: A two-argument callable which will be invoked with the request
being responded to and the tag object on which the render directive
was encountered.
"""
def render(request: Optional[IRequest]) -> "Flattenable":
"""
Get the document for this L{IRenderable}.
@param request: The request in response to which this method is being
invoked.
@return: An object which can be flattened.
"""
class ITemplateLoader(Interface):
"""
A loader for templates; something usable as a value for
L{twisted.web.template.Element}'s C{loader} attribute.
"""
def load() -> List["Flattenable"]:
"""
Load a template suitable for rendering.
@return: a L{list} of flattenable objects, such as byte and unicode
strings, L{twisted.web.template.Element}s and L{IRenderable} providers.
"""
class IResponse(Interface):
"""
An object representing an HTTP response received from an HTTP server.
@since: 11.1
"""
version = Attribute(
"A three-tuple describing the protocol and protocol version "
"of the response. The first element is of type L{str}, the second "
"and third are of type L{int}. For example, C{(b'HTTP', 1, 1)}."
)
code = Attribute("The HTTP status code of this response, as a L{int}.")
phrase = Attribute("The HTTP reason phrase of this response, as a L{str}.")
headers = Attribute("The HTTP response L{Headers} of this response.")
length = Attribute(
"The L{int} number of bytes expected to be in the body of this "
"response or L{UNKNOWN_LENGTH} if the server did not indicate how "
"many bytes to expect. For I{HEAD} responses, this will be 0; if "
"the response includes a I{Content-Length} header, it will be "
"available in C{headers}."
)
request = Attribute("The L{IClientRequest} that resulted in this response.")
previousResponse = Attribute(
"The previous L{IResponse} from a redirect, or L{None} if there was no "
"previous response. This can be used to walk the response or request "
"history for redirections."
)
def deliverBody(protocol):
"""
Register an L{IProtocol<twisted.internet.interfaces.IProtocol>} provider
to receive the response body.
The protocol will be connected to a transport which provides
L{IPushProducer}. The protocol's C{connectionLost} method will be
called with:
- L{ResponseDone}, which indicates that all bytes from the response
have been successfully delivered.
- L{PotentialDataLoss}, which indicates that it cannot be determined
if the entire response body has been delivered. This only occurs
when making requests to HTTP servers which do not set
I{Content-Length} or a I{Transfer-Encoding} in the response.
- L{ResponseFailed}, which indicates that some bytes from the response
were lost. The C{reasons} attribute of the exception may provide
more specific indications as to why.
"""
def setPreviousResponse(response):
"""
Set the reference to the previous L{IResponse}.
The value of the previous response can be read via
L{IResponse.previousResponse}.
"""
class _IRequestEncoder(Interface):
"""
An object encoding data passed to L{IRequest.write}, for example for
compression purpose.
@since: 12.3
"""
def encode(data):
"""
Encode the data given and return the result.
@param data: The content to encode.
@type data: L{str}
@return: The encoded data.
@rtype: L{str}
"""
def finish():
"""
Callback called when the request is closing.
@return: If necessary, the pending data accumulated from previous
C{encode} calls.
@rtype: L{str}
"""
class _IRequestEncoderFactory(Interface):
"""
A factory for returing L{_IRequestEncoder} instances.
@since: 12.3
"""
def encoderForRequest(request):
"""
If applicable, returns a L{_IRequestEncoder} instance which will encode
the request.
"""
class IClientRequest(Interface):
"""
An object representing an HTTP request to make to an HTTP server.
@since: 13.1
"""
method = Attribute(
"The HTTP method for this request, as L{bytes}. For example: "
"C{b'GET'}, C{b'HEAD'}, C{b'POST'}, etc."
)
absoluteURI = Attribute(
"The absolute URI of the requested resource, as L{bytes}; or L{None} "
"if the absolute URI cannot be determined."
)
headers = Attribute(
"Headers to be sent to the server, as "
"a L{twisted.web.http_headers.Headers} instance."
)
class IAgent(Interface):
"""
An agent makes HTTP requests.
The way in which requests are issued is left up to each implementation.
Some may issue them directly to the server indicated by the net location
portion of the request URL. Others may use a proxy specified by system
configuration.
Processing of responses is also left very widely specified. An
implementation may perform no special handling of responses, or it may
implement redirect following or content negotiation, it may implement a
cookie store or automatically respond to authentication challenges. It may
implement many other unforeseen behaviors as well.
It is also intended that L{IAgent} implementations be composable. An
implementation which provides cookie handling features should re-use an
implementation that provides connection pooling and this combination could
be used by an implementation which adds content negotiation functionality.
Some implementations will be completely self-contained, such as those which
actually perform the network operations to send and receive requests, but
most or all other implementations should implement a small number of new
features (perhaps one new feature) and delegate the rest of the
request/response machinery to another implementation.
This allows for great flexibility in the behavior an L{IAgent} will
provide. For example, an L{IAgent} with web browser-like behavior could be
obtained by combining a number of (hypothetical) implementations::
baseAgent = Agent(reactor)
decode = ContentDecoderAgent(baseAgent, [(b"gzip", GzipDecoder())])
cookie = CookieAgent(decode, diskStore.cookie)
authenticate = AuthenticateAgent(
cookie, [diskStore.credentials, GtkAuthInterface()])
cache = CacheAgent(authenticate, diskStore.cache)
redirect = BrowserLikeRedirectAgent(cache, limit=10)
doSomeRequests(cache)
"""
def request(
method: bytes,
uri: bytes,
headers: Optional[Headers] = None,
bodyProducer: Optional[IBodyProducer] = None,
) -> Deferred[IResponse]:
"""
Request the resource at the given location.
@param method: The request method to use, such as C{b"GET"}, C{b"HEAD"},
C{b"PUT"}, C{b"POST"}, etc.
@param uri: The location of the resource to request. This should be an
absolute URI but some implementations may support relative URIs
(with absolute or relative paths). I{HTTP} and I{HTTPS} are the
schemes most likely to be supported but others may be as well.
@param headers: The headers to send with the request (or L{None} to
send no extra headers). An implementation may add its own headers
to this (for example for client identification or content
negotiation).
@param bodyProducer: An object which can generate bytes to make up the
body of this request (for example, the properly encoded contents of
a file for a file upload). Or, L{None} if the request is to have
no body.
@return: A L{Deferred} that fires with an L{IResponse} provider when
the header of the response has been received (regardless of the
response status code) or with a L{Failure} if there is any problem
which prevents that response from being received (including
problems that prevent the request from being sent).
"""
class IPolicyForHTTPS(Interface):
"""
An L{IPolicyForHTTPS} provides a policy for verifying the certificates of
HTTPS connections, in the form of a L{client connection creator
<twisted.internet.interfaces.IOpenSSLClientConnectionCreator>} per network
location.
@since: 14.0
"""
def creatorForNetloc(hostname, port):
"""
Create a L{client connection creator
<twisted.internet.interfaces.IOpenSSLClientConnectionCreator>}
appropriate for the given URL "netloc"; i.e. hostname and port number
pair.
@param hostname: The name of the requested remote host.
@type hostname: L{bytes}
@param port: The number of the requested remote port.
@type port: L{int}
@return: A client connection creator expressing the security
requirements for the given remote host.
@rtype: L{client connection creator
<twisted.internet.interfaces.IOpenSSLClientConnectionCreator>}
"""
class IAgentEndpointFactory(Interface):
"""
An L{IAgentEndpointFactory} provides a way of constructing an endpoint
used for outgoing Agent requests. This is useful in the case of needing to
proxy outgoing connections, or to otherwise vary the transport used.
@since: 15.0
"""
def endpointForURI(uri):
"""
Construct and return an L{IStreamClientEndpoint} for the outgoing
request's connection.
@param uri: The URI of the request.
@type uri: L{twisted.web.client.URI}
@return: An endpoint which will have its C{connect} method called to
issue the request.
@rtype: an L{IStreamClientEndpoint} provider
@raises twisted.internet.error.SchemeNotSupported: If the given
URI's scheme cannot be handled by this factory.
"""
UNKNOWN_LENGTH = "twisted.web.iweb.UNKNOWN_LENGTH"
__all__ = [
"IUsernameDigestHash",
"ICredentialFactory",
"IRequest",
"IBodyProducer",
"IRenderable",
"IResponse",
"_IRequestEncoder",
"_IRequestEncoderFactory",
"IClientRequest",
"UNKNOWN_LENGTH",
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
!.gitignore

View File

@@ -0,0 +1,134 @@
# -*- 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 typing import cast
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: int, brief: str, detail: str) -> None:
super().__init__()
self._code: int = code
self._brief: str = brief
self._detail: str = detail
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")
return renderElement(
request,
# 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)),
),
),
)
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 errorPage(code: int, brief: str, detail: str) -> _ErrorPage:
"""
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()} to override a 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: str = "No Such Resource",
message: str = "Sorry. No luck finding that resource.",
) -> IResource:
"""
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: str = "Forbidden Resource", message: str = "Sorry, resource is forbidden."
) -> IResource:
"""
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)

View File

@@ -0,0 +1,296 @@
# -*- test-case-name: twisted.web.test.test_proxy -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Simplistic HTTP proxy support.
This comes in two main variants - the Proxy and the ReverseProxy.
When a Proxy is in use, a browser trying to connect to a server (say,
www.yahoo.com) will be intercepted by the Proxy, and the proxy will covertly
connect to the server, and return the result.
When a ReverseProxy is in use, the client connects directly to the ReverseProxy
(say, www.yahoo.com) which farms off the request to one of a pool of servers,
and returns the result.
Normally, a Proxy is used on the client end of an Internet connection, while a
ReverseProxy is used on the server end.
"""
from urllib.parse import quote as urlquote, urlparse, urlunparse
from twisted.internet import reactor
from twisted.internet.protocol import ClientFactory
from twisted.web.http import _QUEUED_SENTINEL, HTTPChannel, HTTPClient, Request
from twisted.web.resource import Resource
from twisted.web.server import NOT_DONE_YET
class ProxyClient(HTTPClient):
"""
Used by ProxyClientFactory to implement a simple web proxy.
@ivar _finished: A flag which indicates whether or not the original request
has been finished yet.
"""
_finished = False
def __init__(self, command, rest, version, headers, data, father):
self.father = father
self.command = command
self.rest = rest
if b"proxy-connection" in headers:
del headers[b"proxy-connection"]
headers[b"connection"] = b"close"
headers.pop(b"keep-alive", None)
self.headers = headers
self.data = data
def connectionMade(self):
self.sendCommand(self.command, self.rest)
for header, value in self.headers.items():
self.sendHeader(header, value)
self.endHeaders()
self.transport.write(self.data)
def handleStatus(self, version, code, message):
self.father.setResponseCode(int(code), message)
def handleHeader(self, key, value):
# t.web.server.Request sets default values for these headers in its
# 'process' method. When these headers are received from the remote
# server, they ought to override the defaults, rather than append to
# them.
if key.lower() in [b"server", b"date", b"content-type"]:
self.father.responseHeaders.setRawHeaders(key, [value])
else:
self.father.responseHeaders.addRawHeader(key, value)
def handleResponsePart(self, buffer):
self.father.write(buffer)
def handleResponseEnd(self):
"""
Finish the original request, indicating that the response has been
completely written to it, and disconnect the outgoing transport.
"""
if not self._finished:
self._finished = True
self.father.finish()
self.transport.loseConnection()
class ProxyClientFactory(ClientFactory):
"""
Used by ProxyRequest to implement a simple web proxy.
"""
# Type is wrong. See: https://twistedmatrix.com/trac/ticket/10006
protocol = ProxyClient # type: ignore[assignment]
def __init__(self, command, rest, version, headers, data, father):
self.father = father
self.command = command
self.rest = rest
self.headers = headers
self.data = data
self.version = version
def buildProtocol(self, addr):
return self.protocol(
self.command, self.rest, self.version, self.headers, self.data, self.father
)
def clientConnectionFailed(self, connector, reason):
"""
Report a connection failure in a response to the incoming request as
an error.
"""
self.father.setResponseCode(501, b"Gateway error")
self.father.responseHeaders.addRawHeader(b"Content-Type", b"text/html")
self.father.write(b"<H1>Could not connect</H1>")
self.father.finish()
class ProxyRequest(Request):
"""
Used by Proxy to implement a simple web proxy.
@ivar reactor: the reactor used to create connections.
@type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
"""
protocols = {b"http": ProxyClientFactory}
ports = {b"http": 80}
def __init__(self, channel, queued=_QUEUED_SENTINEL, reactor=reactor):
Request.__init__(self, channel, queued)
self.reactor = reactor
def process(self):
parsed = urlparse(self.uri)
protocol = parsed[0]
host = parsed[1].decode("ascii")
port = self.ports[protocol]
if ":" in host:
host, port = host.split(":")
port = int(port)
rest = urlunparse((b"", b"") + parsed[2:])
if not rest:
rest = rest + b"/"
class_ = self.protocols[protocol]
headers = self.getAllHeaders().copy()
if b"host" not in headers:
headers[b"host"] = host.encode("ascii")
self.content.seek(0, 0)
s = self.content.read()
clientFactory = class_(self.method, rest, self.clientproto, headers, s, self)
self.reactor.connectTCP(host, port, clientFactory)
class Proxy(HTTPChannel):
"""
This class implements a simple web proxy.
Since it inherits from L{twisted.web.http.HTTPChannel}, to use it you
should do something like this::
from twisted.web import http
f = http.HTTPFactory()
f.protocol = Proxy
Make the HTTPFactory a listener on a port as per usual, and you have
a fully-functioning web proxy!
"""
requestFactory = ProxyRequest
class ReverseProxyRequest(Request):
"""
Used by ReverseProxy to implement a simple reverse proxy.
@ivar proxyClientFactoryClass: a proxy client factory class, used to create
new connections.
@type proxyClientFactoryClass: L{ClientFactory}
@ivar reactor: the reactor used to create connections.
@type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
"""
proxyClientFactoryClass = ProxyClientFactory
def __init__(self, channel, queued=_QUEUED_SENTINEL, reactor=reactor):
Request.__init__(self, channel, queued)
self.reactor = reactor
def process(self):
"""
Handle this request by connecting to the proxied server and forwarding
it there, then forwarding the response back as the response to this
request.
"""
self.requestHeaders.setRawHeaders(b"host", [self.factory.host.encode("ascii")])
clientFactory = self.proxyClientFactoryClass(
self.method,
self.uri,
self.clientproto,
self.getAllHeaders(),
self.content.read(),
self,
)
self.reactor.connectTCP(self.factory.host, self.factory.port, clientFactory)
class ReverseProxy(HTTPChannel):
"""
Implements a simple reverse proxy.
For details of usage, see the file examples/reverse-proxy.py.
"""
requestFactory = ReverseProxyRequest
class ReverseProxyResource(Resource):
"""
Resource that renders the results gotten from another server
Put this resource in the tree to cause everything below it to be relayed
to a different server.
@ivar proxyClientFactoryClass: a proxy client factory class, used to create
new connections.
@type proxyClientFactoryClass: L{ClientFactory}
@ivar reactor: the reactor used to create connections.
@type reactor: object providing L{twisted.internet.interfaces.IReactorTCP}
"""
proxyClientFactoryClass = ProxyClientFactory
def __init__(self, host, port, path, reactor=reactor):
"""
@param host: the host of the web server to proxy.
@type host: C{str}
@param port: the port of the web server to proxy.
@type port: C{port}
@param path: the base path to fetch data from. Note that you shouldn't
put any trailing slashes in it, it will be added automatically in
request. For example, if you put B{/foo}, a request on B{/bar} will
be proxied to B{/foo/bar}. Any required encoding of special
characters (such as " " or "/") should have been done already.
@type path: C{bytes}
"""
Resource.__init__(self)
self.host = host
self.port = port
self.path = path
self.reactor = reactor
def getChild(self, path, request):
"""
Create and return a proxy resource with the same proxy configuration
as this one, except that its path also contains the segment given by
C{path} at the end.
"""
return ReverseProxyResource(
self.host,
self.port,
self.path + b"/" + urlquote(path, safe=b"").encode("utf-8"),
self.reactor,
)
def render(self, request):
"""
Render a request by forwarding it to the proxied server.
"""
# RFC 2616 tells us that we can omit the port if it's the default port,
# but we have to provide it otherwise
if self.port == 80:
host = self.host
else:
host = "%s:%d" % (self.host, self.port)
request.requestHeaders.setRawHeaders(b"host", [host.encode("ascii")])
request.content.seek(0, 0)
qs = urlparse(request.uri)[4]
if qs:
rest = self.path + b"?" + qs
else:
rest = self.path
clientFactory = self.proxyClientFactoryClass(
request.method,
rest,
request.clientproto,
request.getAllHeaders(),
request.content.read(),
request,
)
self.reactor.connectTCP(self.host, self.port, clientFactory)
return NOT_DONE_YET

View File

@@ -0,0 +1,460 @@
# -*- 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 annotations
__all__ = [
"IResource",
"getChildForRequest",
"Resource",
"ErrorPage",
"NoResource",
"ForbiddenResource",
"EncodingResourceWrapper",
]
import warnings
from typing import Sequence
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 deprecated
from twisted.python.reflect import prefixedMethodNames
from twisted.web._responses import FORBIDDEN, NOT_FOUND
from twisted.web.error import UnsupportedMethod
class IResource(Interface):
"""
A web resource.
"""
isLeaf = Attribute(
"""
Signal if this IResource implementor is a "leaf node" or not. If True,
getChildWithDefault will not be called on this Resource.
"""
)
def getChildWithDefault(name, request):
"""
Return a child with the given name for the given request.
This is the external interface used by the Resource publishing
machinery. If implementing IResource without subclassing
Resource, it must be provided. However, if subclassing Resource,
getChild overridden instead.
@param name: A single path component from a requested URL. For example,
a request for I{http://example.com/foo/bar} will result in calls to
this method with C{b"foo"} and C{b"bar"} as values for this
argument.
@type name: C{bytes}
@param request: A representation of all of the information about the
request that is being made for this child.
@type request: L{twisted.web.server.Request}
"""
def putChild(path: bytes, child: "IResource") -> None:
"""
Put a child L{IResource} implementor at the given path.
@param path: A single path component, to be interpreted relative to the
path this resource is found at, at which to put the given child.
For example, if resource A can be found at I{http://example.com/foo}
then a call like C{A.putChild(b"bar", B)} will make resource B
available at I{http://example.com/foo/bar}.
The path component is I{not} URL-encoded -- pass C{b'foo bar'}
rather than C{b'foo%20bar'}.
"""
def render(request):
"""
Render a request. This is called on the leaf resource for a request.
@return: Either C{server.NOT_DONE_YET} to indicate an asynchronous or a
C{bytes} instance to write as the response to the request. If
C{NOT_DONE_YET} is returned, at some point later (for example, in a
Deferred callback) call C{request.write(b"<html>")} to write data to
the request, and C{request.finish()} to send the data to the
browser.
@raise twisted.web.error.UnsupportedMethod: If the HTTP verb
requested is not supported by this resource.
"""
def getChildForRequest(resource, request):
"""
Traverse resource tree to find who will handle the request.
"""
while request.postpath and not resource.isLeaf:
pathElement = request.postpath.pop(0)
request.prepath.append(pathElement)
resource = resource.getChildWithDefault(pathElement, request)
return resource
@implementer(IResource)
class Resource:
"""
Define a web-accessible resource.
This serves two main purposes: one is to provide a standard representation
for what HTTP specification calls an 'entity', and the other is to provide
an abstract directory structure for URL retrieval.
"""
entityType = IResource
allowedMethods: Sequence[bytes]
server = None
def __init__(self):
"""
Initialize.
"""
self.children = {}
isLeaf = 0
### Abstract Collection Interface
def listStaticNames(self):
return list(self.children.keys())
def listStaticEntities(self):
return list(self.children.items())
def listNames(self):
return list(self.listStaticNames()) + self.listDynamicNames()
def listEntities(self):
return list(self.listStaticEntities()) + self.listDynamicEntities()
def listDynamicNames(self):
return []
def listDynamicEntities(self, request=None):
return []
def getStaticEntity(self, name):
return self.children.get(name)
def getDynamicEntity(self, name, request):
if name not in self.children:
return self.getChild(name, request)
else:
return None
def delEntity(self, name):
del self.children[name]
def reallyPutEntity(self, name, entity):
self.children[name] = entity
# Concrete HTTP interface
def getChild(self, path, request):
"""
Retrieve a 'child' resource from me.
Implement this to create dynamic resource generation -- resources which
are always available may be registered with self.putChild().
This will not be called if the class-level variable 'isLeaf' is set in
your subclass; instead, the 'postpath' attribute of the request will be
left as a list of the remaining path elements.
For example, the URL /foo/bar/baz will normally be::
| site.resource.getChild('foo').getChild('bar').getChild('baz').
However, if the resource returned by 'bar' has isLeaf set to true, then
the getChild call will never be made on it.
Parameters and return value have the same meaning and requirements as
those defined by L{IResource.getChildWithDefault}.
"""
return _UnsafeNoResource()
def getChildWithDefault(self, path, request):
"""
Retrieve a static or dynamically generated child resource from me.
First checks if a resource was added manually by putChild, and then
call getChild to check for dynamic resources. Only override if you want
to affect behaviour of all child lookups, rather than just dynamic
ones.
This will check to see if I have a pre-registered child resource of the
given name, and call getChild if I do not.
@see: L{IResource.getChildWithDefault}
"""
if path in self.children:
return self.children[path]
return self.getChild(path, request)
def getChildForRequest(self, request):
"""
Deprecated in favor of L{getChildForRequest}.
@see: L{twisted.web.resource.getChildForRequest}.
"""
warnings.warn(
"Please use module level getChildForRequest.", DeprecationWarning, 2
)
return getChildForRequest(self, request)
def putChild(self, path: bytes, child: IResource) -> None:
"""
Register a static child.
You almost certainly don't want '/' in your path. If you
intended to have the root of a folder, e.g. /foo/, you want
path to be ''.
@param path: A single path component.
@param child: The child resource to register.
@see: L{IResource.putChild}
"""
if not isinstance(path, bytes):
raise TypeError(f"Path segment must be bytes, but {path!r} is {type(path)}")
self.children[path] = child
# IResource is incomplete and doesn't mention this server attribute, see
# https://github.com/twisted/twisted/issues/11717
child.server = self.server # type: ignore[attr-defined]
def render(self, request):
"""
Render a given resource. See L{IResource}'s render method.
I delegate to methods of self with the form 'render_METHOD'
where METHOD is the HTTP that was used to make the
request. Examples: render_GET, render_HEAD, render_POST, and
so on. Generally you should implement those methods instead of
overriding this one.
render_METHOD methods are expected to return a byte string which will be
the rendered page, unless the return value is C{server.NOT_DONE_YET}, in
which case it is this class's responsibility to write the results using
C{request.write(data)} and then call C{request.finish()}.
Old code that overrides render() directly is likewise expected
to return a byte string or NOT_DONE_YET.
@see: L{IResource.render}
"""
m = getattr(self, "render_" + nativeString(request.method), None)
if not m:
try:
allowedMethods = self.allowedMethods
except AttributeError:
allowedMethods = _computeAllowedMethods(self)
raise UnsupportedMethod(allowedMethods)
return m(request)
def render_HEAD(self, request):
"""
Default handling of HEAD method.
I just return self.render_GET(request). When method is HEAD,
the framework will handle this correctly.
"""
return self.render_GET(request)
def _computeAllowedMethods(resource):
"""
Compute the allowed methods on a C{Resource} based on defined render_FOO
methods. Used when raising C{UnsupportedMethod} but C{Resource} does
not define C{allowedMethods} attribute.
"""
allowedMethods = []
for name in prefixedMethodNames(resource.__class__, "render_"):
# Potentially there should be an API for encode('ascii') in this
# situation - an API for taking a Python native string (bytes on Python
# 2, text on Python 3) and returning a socket-compatible string type.
allowedMethods.append(name.encode("ascii"))
return allowedMethods
class _UnsafeErrorPageBase(Resource):
"""
Base class for deprecated error page resources.
@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{_UnsafeErrorPage.__init__}.
- C{"brief"}: The brief description passed to
L{_UnsafeErrorPage.__init__}.
- C{"detail"}: The detailed description passed to
L{_UnsafeErrorPage.__init__}.
@ivar code: An integer status code which will be used for the response.
@type code: C{int}
@ivar brief: A short string which will be included in the response body as
the page title.
@type brief: C{str}
@ivar detail: A longer string which will be included in the response body.
@type detail: C{str}
"""
template = """
<html>
<head><title>%(code)s - %(brief)s</title></head>
<body>
<h1>%(brief)s</h1>
<p>%(detail)s</p>
</body>
</html>
"""
def __init__(self, status, brief, detail):
Resource.__init__(self)
self.code = status
self.brief = brief
self.detail = detail
def render(self, request):
request.setResponseCode(self.code)
request.setHeader(b"content-type", b"text/html; charset=utf-8")
interpolated = self.template % dict(
code=self.code, brief=self.brief, detail=self.detail
)
if isinstance(interpolated, str):
return interpolated.encode("utf-8")
return interpolated
def getChild(self, chnam, request):
return self
class _UnsafeErrorPage(_UnsafeErrorPageBase):
"""
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 22.10.0 because it permits HTML injection; use
L{twisted.web.pages.errorPage} instead.
"""
@deprecated(
Version("Twisted", 22, 10, 0),
"Use twisted.web.pages.errorPage instead, which properly escapes HTML.",
)
def __init__(self, status, brief, detail):
_UnsafeErrorPageBase.__init__(self, status, brief, detail)
class _UnsafeNoResource(_UnsafeErrorPageBase):
"""
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 22.10.0 because it permits HTML injection; use
L{twisted.web.pages.notFound} instead.
"""
@deprecated(
Version("Twisted", 22, 10, 0),
"Use twisted.web.pages.notFound instead, which properly escapes HTML.",
)
def __init__(self, message="Sorry. No luck finding that resource."):
_UnsafeErrorPageBase.__init__(self, NOT_FOUND, "No Such Resource", message)
class _UnsafeForbiddenResource(_UnsafeErrorPageBase):
"""
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 22.10.0 because it permits HTML injection; use
L{twisted.web.pages.forbidden} instead.
"""
@deprecated(
Version("Twisted", 22, 10, 0),
"Use twisted.web.pages.forbidden instead, which properly escapes HTML.",
)
def __init__(self, message="Sorry, resource is forbidden."):
_UnsafeErrorPageBase.__init__(self, FORBIDDEN, "Forbidden Resource", message)
# Deliberately undocumented public aliases. See GHSA-vg46-2rrj-3647.
ErrorPage = _UnsafeErrorPage
NoResource = _UnsafeNoResource
ForbiddenResource = _UnsafeForbiddenResource
class _IEncodingResource(Interface):
"""
A resource which knows about L{_IRequestEncoderFactory}.
@since: 12.3
"""
def getEncoder(request):
"""
Parse the request and return an encoder if applicable, using
L{_IRequestEncoderFactory.encoderForRequest}.
@return: A L{_IRequestEncoder}, or L{None}.
"""
@implementer(_IEncodingResource)
class EncodingResourceWrapper(proxyForInterface(IResource)): # type: ignore[misc]
"""
Wrap a L{IResource}, potentially applying an encoding to the response body
generated.
Note that the returned children resources won't be wrapped, so you have to
explicitly wrap them if you want the encoding to be applied.
@ivar encoders: A list of
L{_IRequestEncoderFactory<twisted.web.iweb._IRequestEncoderFactory>}
returning L{_IRequestEncoder<twisted.web.iweb._IRequestEncoder>} that
may transform the data passed to C{Request.write}. The list must be
sorted in order of priority: the first encoder factory handling the
request will prevent the others from doing the same.
@type encoders: C{list}.
@since: 12.3
"""
def __init__(self, original, encoders):
super().__init__(original)
self._encoders = encoders
def getEncoder(self, request):
"""
Browser the list of encoders looking for one applicable encoder.
"""
for encoderFactory in self._encoders:
encoder = encoderFactory.encoderForRequest(request)
if encoder is not None:
return encoder

View File

@@ -0,0 +1,55 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
#
from twisted.web import resource
class RewriterResource(resource.Resource):
def __init__(self, orig, *rewriteRules):
resource.Resource.__init__(self)
self.resource = orig
self.rewriteRules = list(rewriteRules)
def _rewrite(self, request):
for rewriteRule in self.rewriteRules:
rewriteRule(request)
def getChild(self, path, request):
request.postpath.insert(0, path)
request.prepath.pop()
self._rewrite(request)
path = request.postpath.pop(0)
request.prepath.append(path)
return self.resource.getChildWithDefault(path, request)
def render(self, request):
self._rewrite(request)
return self.resource.render(request)
def tildeToUsers(request):
if request.postpath and request.postpath[0][:1] == "~":
request.postpath[:1] = ["users", request.postpath[0][1:]]
request.path = "/" + "/".join(request.prepath + request.postpath)
def alias(aliasPath, sourcePath):
"""
I am not a very good aliaser. But I'm the best I can be. If I'm
aliasing to a Resource that generates links, and it uses any parts
of request.prepath to do so, the links will not be relative to the
aliased path, but rather to the aliased-to path. That I can't
alias static.File directory listings that nicely. However, I can
still be useful, as many resources will play nice.
"""
sourcePath = sourcePath.split("/")
aliasPath = aliasPath.split("/")
def rewriter(request):
if request.postpath[: len(aliasPath)] == aliasPath:
after = request.postpath[len(aliasPath) :]
request.postpath = sourcePath + after
request.path = "/" + "/".join(request.prepath + request.postpath)
return rewriter

View File

@@ -0,0 +1,193 @@
# -*- test-case-name: twisted.web.test.test_script -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
I contain PythonScript, which is a very simple python script resource.
"""
import os
import traceback
from io import StringIO
from twisted import copyright
from twisted.python.compat import execfile, networkString
from twisted.python.filepath import _coerceToFilesystemEncoding
from twisted.web import http, resource, server, static, util
rpyNoResource = """<p>You forgot to assign to the variable "resource" in your script. For example:</p>
<pre>
# MyCoolWebApp.rpy
import mygreatresource
resource = mygreatresource.MyGreatResource()
</pre>
"""
class AlreadyCached(Exception):
"""
This exception is raised when a path has already been cached.
"""
class CacheScanner:
def __init__(self, path, registry):
self.path = path
self.registry = registry
self.doCache = 0
def cache(self):
c = self.registry.getCachedPath(self.path)
if c is not None:
raise AlreadyCached(c)
self.recache()
def recache(self):
self.doCache = 1
noRsrc = resource._UnsafeErrorPage(500, "Whoops! Internal Error", rpyNoResource)
def ResourceScript(path, registry):
"""
I am a normal py file which must define a 'resource' global, which should
be an instance of (a subclass of) web.resource.Resource; it will be
renderred.
"""
cs = CacheScanner(path, registry)
glob = {
"__file__": _coerceToFilesystemEncoding("", path),
"resource": noRsrc,
"registry": registry,
"cache": cs.cache,
"recache": cs.recache,
}
try:
execfile(path, glob, glob)
except AlreadyCached as ac:
return ac.args[0]
rsrc = glob["resource"]
if cs.doCache and rsrc is not noRsrc:
registry.cachePath(path, rsrc)
return rsrc
def ResourceTemplate(path, registry):
from quixote import ptl_compile
glob = {
"__file__": _coerceToFilesystemEncoding("", path),
"resource": resource._UnsafeErrorPage(
500, "Whoops! Internal Error", rpyNoResource
),
"registry": registry,
}
with open(path) as f: # Not closed by quixote as of 2.9.1
e = ptl_compile.compile_template(f, path)
code = compile(e, "<source>", "exec")
eval(code, glob, glob)
return glob["resource"]
class ResourceScriptWrapper(resource.Resource):
def __init__(self, path, registry=None):
resource.Resource.__init__(self)
self.path = path
self.registry = registry or static.Registry()
def render(self, request):
res = ResourceScript(self.path, self.registry)
return res.render(request)
def getChildWithDefault(self, path, request):
res = ResourceScript(self.path, self.registry)
return res.getChildWithDefault(path, request)
class ResourceScriptDirectory(resource.Resource):
"""
L{ResourceScriptDirectory} is a resource which serves scripts from a
filesystem directory. File children of a L{ResourceScriptDirectory} will
be served using L{ResourceScript}. Directory children will be served using
another L{ResourceScriptDirectory}.
@ivar path: A C{str} giving the filesystem path in which children will be
looked up.
@ivar registry: A L{static.Registry} instance which will be used to decide
how to interpret scripts found as children of this resource.
"""
def __init__(self, pathname, registry=None):
resource.Resource.__init__(self)
self.path = pathname
self.registry = registry or static.Registry()
def getChild(self, path, request):
fn = os.path.join(self.path, path)
if os.path.isdir(fn):
return ResourceScriptDirectory(fn, self.registry)
if os.path.exists(fn):
return ResourceScript(fn, self.registry)
return resource._UnsafeNoResource()
def render(self, request):
return resource._UnsafeNoResource().render(request)
class PythonScript(resource.Resource):
"""
I am an extremely simple dynamic resource; an embedded python script.
This will execute a file (usually of the extension '.epy') as Python code,
internal to the webserver.
"""
isLeaf = True
def __init__(self, filename, registry):
"""
Initialize me with a script name.
"""
self.filename = filename
self.registry = registry
def render(self, request):
"""
Render me to a web client.
Load my file, execute it in a special namespace (with 'request' and
'__file__' global vars) and finish the request. Output to the web-page
will NOT be handled with print - standard output goes to the log - but
with request.write.
"""
request.setHeader(
b"x-powered-by", networkString("Twisted/%s" % copyright.version)
)
namespace = {
"request": request,
"__file__": _coerceToFilesystemEncoding("", self.filename),
"registry": self.registry,
}
try:
execfile(self.filename, namespace, namespace)
except OSError as e:
if e.errno == 2: # file not found
request.setResponseCode(http.NOT_FOUND)
request.write(
resource._UnsafeNoResource("File not found.").render(request)
)
except BaseException:
io = StringIO()
traceback.print_exc(file=io)
output = util._PRE(io.getvalue())
output = output.encode("utf8")
request.write(output)
request.finish()
return server.NOT_DONE_YET

View File

@@ -0,0 +1,891 @@
# -*- test-case-name: twisted.web.test.test_web -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
This is a web server which integrates with the twisted.internet infrastructure.
@var NOT_DONE_YET: A token value which L{twisted.web.resource.IResource.render}
implementations can return to indicate that the application will later call
C{.write} and C{.finish} to complete the request, and that the HTTP
connection should be left open.
@type NOT_DONE_YET: Opaque; do not depend on any particular type for this
value.
"""
import copy
import os
import re
import zlib
from binascii import hexlify
from html import escape
from typing import List, Optional
from urllib.parse import quote as _quote
from zope.interface import implementer
from twisted import copyright
from twisted.internet import address, interfaces
from twisted.internet.error import AlreadyCalled, AlreadyCancelled
from twisted.logger import Logger
from twisted.python import components, failure, reflect
from twisted.python.compat import nativeString, networkString
from twisted.spread.pb import Copyable, ViewPoint
from twisted.web import http, iweb, resource, util
from twisted.web.error import UnsupportedMethod
from twisted.web.http import (
NO_CONTENT,
NOT_MODIFIED,
HTTPFactory,
Request as _HTTPRequest,
datetimeToString,
unquote,
)
NOT_DONE_YET = 1
__all__ = [
"supportedMethods",
"Request",
"Session",
"Site",
"version",
"NOT_DONE_YET",
"GzipEncoderFactory",
]
# Support for other methods may be implemented on a per-resource basis.
supportedMethods = (b"GET", b"HEAD", b"POST")
def quote(string, *args, **kwargs):
return _quote(string.decode("charmap"), *args, **kwargs).encode("charmap")
def _addressToTuple(addr):
if isinstance(addr, address.IPv4Address):
return ("INET", addr.host, addr.port)
elif isinstance(addr, address.UNIXAddress):
return ("UNIX", addr.name)
else:
return tuple(addr)
@implementer(iweb.IRequest)
class Request(Copyable, http.Request, components.Componentized):
"""
An HTTP request.
@ivar defaultContentType: A L{bytes} giving the default I{Content-Type}
value to send in responses if no other value is set. L{None} disables
the default.
@ivar _insecureSession: The L{Session} object representing state that will
be transmitted over plain-text HTTP.
@ivar _secureSession: The L{Session} object representing the state that
will be transmitted only over HTTPS.
"""
defaultContentType: Optional[bytes] = b"text/html"
site = None
appRootURL = None
prepath: Optional[List[bytes]] = None
postpath: Optional[List[bytes]] = None
__pychecker__ = "unusednames=issuer"
_inFakeHead = False
_encoder = None
_log = Logger()
def __init__(self, *args, **kw):
_HTTPRequest.__init__(self, *args, **kw)
components.Componentized.__init__(self)
def getStateToCopyFor(self, issuer):
x = self.__dict__.copy()
del x["transport"]
# XXX refactor this attribute out; it's from protocol
# del x['server']
del x["channel"]
del x["content"]
del x["site"]
self.content.seek(0, 0)
x["content_data"] = self.content.read()
x["remote"] = ViewPoint(issuer, self)
# Address objects aren't jellyable
x["host"] = _addressToTuple(x["host"])
x["client"] = _addressToTuple(x["client"])
# Header objects also aren't jellyable.
x["requestHeaders"] = list(x["requestHeaders"].getAllRawHeaders())
return x
# HTML generation helpers
def sibLink(self, name):
"""
Return the text that links to a sibling of the requested resource.
@param name: The sibling resource
@type name: C{bytes}
@return: A relative URL.
@rtype: C{bytes}
"""
if self.postpath:
return (len(self.postpath) * b"../") + name
else:
return name
def childLink(self, name):
"""
Return the text that links to a child of the requested resource.
@param name: The child resource
@type name: C{bytes}
@return: A relative URL.
@rtype: C{bytes}
"""
lpp = len(self.postpath)
if lpp > 1:
return ((lpp - 1) * b"../") + name
elif lpp == 1:
return name
else: # lpp == 0
if len(self.prepath) and self.prepath[-1]:
return self.prepath[-1] + b"/" + name
else:
return name
def gotLength(self, length):
"""
Called when HTTP channel got length of content in this request.
This method is not intended for users.
@param length: The length of the request body, as indicated by the
request headers. L{None} if the request headers do not indicate a
length.
"""
try:
getContentFile = self.channel.site.getContentFile
except AttributeError:
_HTTPRequest.gotLength(self, length)
else:
self.content = getContentFile(length)
def process(self):
"""
Process a request.
Find the addressed resource in this request's L{Site},
and call L{self.render()<Request.render()>} with it.
@see: L{Site.getResourceFor()}
"""
# get site from channel
self.site = self.channel.site
# set various default headers
self.setHeader(b"Server", version)
self.setHeader(b"Date", datetimeToString())
# Resource Identification
self.prepath = []
self.postpath = list(map(unquote, self.path[1:].split(b"/")))
# Short-circuit for requests whose path is '*'.
if self.path == b"*":
self._handleStar()
return
try:
resrc = self.site.getResourceFor(self)
if resource._IEncodingResource.providedBy(resrc):
encoder = resrc.getEncoder(self)
if encoder is not None:
self._encoder = encoder
self.render(resrc)
except BaseException:
self.processingFailed(failure.Failure())
def write(self, data):
"""
Write data to the transport (if not responding to a HEAD request).
@param data: A string to write to the response.
@type data: L{bytes}
"""
if not self.startedWriting:
# Before doing the first write, check to see if a default
# Content-Type header should be supplied. We omit it on
# NOT_MODIFIED and NO_CONTENT responses. We also omit it if there
# is a Content-Length header set to 0, as empty bodies don't need
# a content-type.
needsCT = self.code not in (NOT_MODIFIED, NO_CONTENT)
contentType = self.responseHeaders.getRawHeaders(b"Content-Type")
contentLength = self.responseHeaders.getRawHeaders(b"Content-Length")
contentLengthZero = contentLength and (contentLength[0] == b"0")
if (
needsCT
and contentType is None
and self.defaultContentType is not None
and not contentLengthZero
):
self.responseHeaders.setRawHeaders(
b"Content-Type", [self.defaultContentType]
)
# Only let the write happen if we're not generating a HEAD response by
# faking out the request method. Note, if we are doing that,
# startedWriting will never be true, and the above logic may run
# multiple times. It will only actually change the responseHeaders
# once though, so it's still okay.
if not self._inFakeHead:
if self._encoder:
data = self._encoder.encode(data)
_HTTPRequest.write(self, data)
def finish(self):
"""
Override L{twisted.web.http.Request.finish} for possible encoding.
"""
if self._encoder:
data = self._encoder.finish()
if data:
_HTTPRequest.write(self, data)
return _HTTPRequest.finish(self)
def render(self, resrc):
"""
Ask a resource to render itself.
If the resource does not support the requested method,
generate a C{NOT IMPLEMENTED} or C{NOT ALLOWED} response.
@param resrc: The resource to render.
@type resrc: L{twisted.web.resource.IResource}
@see: L{IResource.render()<twisted.web.resource.IResource.render()>}
"""
try:
body = resrc.render(self)
except UnsupportedMethod as e:
allowedMethods = e.allowedMethods
if (self.method == b"HEAD") and (b"GET" in allowedMethods):
# We must support HEAD (RFC 2616, 5.1.1). If the
# resource doesn't, fake it by giving the resource
# a 'GET' request and then return only the headers,
# not the body.
self._log.info(
"Using GET to fake a HEAD request for {resrc}", resrc=resrc
)
self.method = b"GET"
self._inFakeHead = True
body = resrc.render(self)
if body is NOT_DONE_YET:
self._log.info(
"Tried to fake a HEAD request for {resrc}, but "
"it got away from me.",
resrc=resrc,
)
# Oh well, I guess we won't include the content length.
else:
self.setHeader(b"Content-Length", b"%d" % (len(body),))
self._inFakeHead = False
self.method = b"HEAD"
self.write(b"")
self.finish()
return
if self.method in (supportedMethods):
# We MUST include an Allow header
# (RFC 2616, 10.4.6 and 14.7)
self.setHeader(b"Allow", b", ".join(allowedMethods))
s = (
"""Your browser approached me (at %(URI)s) with"""
""" the method "%(method)s". I only allow"""
""" the method%(plural)s %(allowed)s here."""
% {
"URI": escape(nativeString(self.uri)),
"method": nativeString(self.method),
"plural": ((len(allowedMethods) > 1) and "s") or "",
"allowed": ", ".join([nativeString(x) for x in allowedMethods]),
}
)
epage = resource._UnsafeErrorPage(
http.NOT_ALLOWED, "Method Not Allowed", s
)
body = epage.render(self)
else:
epage = resource._UnsafeErrorPage(
http.NOT_IMPLEMENTED,
"Huh?",
"I don't know how to treat a %s request."
% (escape(self.method.decode("charmap")),),
)
body = epage.render(self)
# end except UnsupportedMethod
if body is NOT_DONE_YET:
return
if not isinstance(body, bytes):
body = resource._UnsafeErrorPage(
http.INTERNAL_SERVER_ERROR,
"Request did not return bytes",
"Request: "
# GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
+ util._PRE(reflect.safe_repr(self))
+ "<br />"
+ "Resource: "
+ util._PRE(reflect.safe_repr(resrc))
+ "<br />"
+ "Value: "
+ util._PRE(reflect.safe_repr(body)),
).render(self)
if self.method == b"HEAD":
if len(body) > 0:
# This is a Bad Thing (RFC 2616, 9.4)
self._log.info(
"Warning: HEAD request {slf} for resource {resrc} is"
" returning a message body. I think I'll eat it.",
slf=self,
resrc=resrc,
)
self.setHeader(b"Content-Length", b"%d" % (len(body),))
self.write(b"")
else:
self.setHeader(b"Content-Length", b"%d" % (len(body),))
self.write(body)
self.finish()
def processingFailed(self, reason):
"""
Finish this request with an indication that processing failed and
possibly display a traceback.
@param reason: Reason this request has failed.
@type reason: L{twisted.python.failure.Failure}
@return: The reason passed to this method.
@rtype: L{twisted.python.failure.Failure}
"""
self._log.failure("", failure=reason)
if self.site.displayTracebacks:
body = (
b"<html><head><title>web.Server Traceback"
b" (most recent call last)</title></head>"
b"<body><b>web.Server Traceback"
b" (most recent call last):</b>\n\n"
+ util.formatFailure(reason)
+ b"\n\n</body></html>\n"
)
else:
body = (
b"<html><head><title>Processing Failed"
b"</title></head><body>"
b"<b>Processing Failed</b></body></html>"
)
self.setResponseCode(http.INTERNAL_SERVER_ERROR)
self.setHeader(b"Content-Type", b"text/html")
self.setHeader(b"Content-Length", b"%d" % (len(body),))
self.write(body)
self.finish()
return reason
def view_write(self, issuer, data):
"""Remote version of write; same interface."""
self.write(data)
def view_finish(self, issuer):
"""Remote version of finish; same interface."""
self.finish()
def view_addCookie(self, issuer, k, v, **kwargs):
"""Remote version of addCookie; same interface."""
self.addCookie(k, v, **kwargs)
def view_setHeader(self, issuer, k, v):
"""Remote version of setHeader; same interface."""
self.setHeader(k, v)
def view_setLastModified(self, issuer, when):
"""Remote version of setLastModified; same interface."""
self.setLastModified(when)
def view_setETag(self, issuer, tag):
"""Remote version of setETag; same interface."""
self.setETag(tag)
def view_setResponseCode(self, issuer, code, message=None):
"""
Remote version of setResponseCode; same interface.
"""
self.setResponseCode(code, message)
def view_registerProducer(self, issuer, producer, streaming):
"""Remote version of registerProducer; same interface.
(requires a remote producer.)
"""
self.registerProducer(_RemoteProducerWrapper(producer), streaming)
def view_unregisterProducer(self, issuer):
self.unregisterProducer()
### these calls remain local
_secureSession = None
_insecureSession = None
@property
def session(self):
"""
If a session has already been created or looked up with
L{Request.getSession}, this will return that object. (This will always
be the session that matches the security of the request; so if
C{forceNotSecure} is used on a secure request, this will not return
that session.)
@return: the session attribute
@rtype: L{Session} or L{None}
"""
if self.isSecure():
return self._secureSession
else:
return self._insecureSession
def getSession(self, sessionInterface=None, forceNotSecure=False):
"""
Check if there is a session cookie, and if not, create it.
By default, the cookie with be secure for HTTPS requests and not secure
for HTTP requests. If for some reason you need access to the insecure
cookie from a secure request you can set C{forceNotSecure = True}.
@param forceNotSecure: Should we retrieve a session that will be
transmitted over HTTP, even if this L{Request} was delivered over
HTTPS?
@type forceNotSecure: L{bool}
"""
# Make sure we aren't creating a secure session on a non-secure page
secure = self.isSecure() and not forceNotSecure
if not secure:
cookieString = b"TWISTED_SESSION"
sessionAttribute = "_insecureSession"
else:
cookieString = b"TWISTED_SECURE_SESSION"
sessionAttribute = "_secureSession"
session = getattr(self, sessionAttribute)
if session is not None:
# We have a previously created session.
try:
# Refresh the session, to keep it alive.
session.touch()
except (AlreadyCalled, AlreadyCancelled):
# Session has already expired.
session = None
if session is None:
# No session was created yet for this request.
cookiename = b"_".join([cookieString] + self.sitepath)
sessionCookie = self.getCookie(cookiename)
if sessionCookie:
try:
session = self.site.getSession(sessionCookie)
except KeyError:
pass
# if it still hasn't been set, fix it up.
if not session:
session = self.site.makeSession()
self.addCookie(cookiename, session.uid, path=b"/", secure=secure)
setattr(self, sessionAttribute, session)
if sessionInterface:
return session.getComponent(sessionInterface)
return session
def _prePathURL(self, prepath):
port = self.getHost().port
if self.isSecure():
default = 443
else:
default = 80
if port == default:
hostport = ""
else:
hostport = ":%d" % port
prefix = networkString(
"http%s://%s%s/"
% (
self.isSecure() and "s" or "",
nativeString(self.getRequestHostname()),
hostport,
)
)
path = b"/".join([quote(segment, safe=b"") for segment in prepath])
return prefix + path
def prePathURL(self):
return self._prePathURL(self.prepath)
def URLPath(self):
from twisted.python import urlpath
return urlpath.URLPath.fromRequest(self)
def rememberRootURL(self):
"""
Remember the currently-processed part of the URL for later
recalling.
"""
url = self._prePathURL(self.prepath[:-1])
self.appRootURL = url
def getRootURL(self):
"""
Get a previously-remembered URL.
@return: An absolute URL.
@rtype: L{bytes}
"""
return self.appRootURL
def _handleStar(self):
"""
Handle receiving a request whose path is '*'.
RFC 7231 defines an OPTIONS * request as being something that a client
can send as a low-effort way to probe server capabilities or readiness.
Rather than bother the user with this, we simply fast-path it back to
an empty 200 OK. Any non-OPTIONS verb gets a 405 Method Not Allowed
telling the client they can only use OPTIONS.
"""
if self.method == b"OPTIONS":
self.setResponseCode(http.OK)
else:
self.setResponseCode(http.NOT_ALLOWED)
self.setHeader(b"Allow", b"OPTIONS")
# RFC 7231 says we MUST set content-length 0 when responding to this
# with no body.
self.setHeader(b"Content-Length", b"0")
self.finish()
@implementer(iweb._IRequestEncoderFactory)
class GzipEncoderFactory:
"""
@cvar compressLevel: The compression level used by the compressor, default
to 9 (highest).
@since: 12.3
"""
_gzipCheckRegex = re.compile(rb"(:?^|[\s,])gzip(:?$|[\s,])")
compressLevel = 9
def encoderForRequest(self, request):
"""
Check the headers if the client accepts gzip encoding, and encodes the
request if so.
"""
acceptHeaders = b",".join(
request.requestHeaders.getRawHeaders(b"Accept-Encoding", [])
)
if self._gzipCheckRegex.search(acceptHeaders):
encoding = request.responseHeaders.getRawHeaders(b"Content-Encoding")
if encoding:
encoding = b",".join(encoding + [b"gzip"])
else:
encoding = b"gzip"
request.responseHeaders.setRawHeaders(b"Content-Encoding", [encoding])
return _GzipEncoder(self.compressLevel, request)
@implementer(iweb._IRequestEncoder)
class _GzipEncoder:
"""
An encoder which supports gzip.
@ivar _zlibCompressor: The zlib compressor instance used to compress the
stream.
@ivar _request: A reference to the originating request.
@since: 12.3
"""
_zlibCompressor = None
def __init__(self, compressLevel, request):
self._zlibCompressor = zlib.compressobj(
compressLevel, zlib.DEFLATED, 16 + zlib.MAX_WBITS
)
self._request = request
def encode(self, data):
"""
Write to the request, automatically compressing data on the fly.
"""
if not self._request.startedWriting:
# Remove the content-length header, we can't honor it
# because we compress on the fly.
self._request.responseHeaders.removeHeader(b"Content-Length")
return self._zlibCompressor.compress(data)
def finish(self):
"""
Finish handling the request request, flushing any data from the zlib
buffer.
"""
remain = self._zlibCompressor.flush()
self._zlibCompressor = None
return remain
class _RemoteProducerWrapper:
def __init__(self, remote):
self.resumeProducing = remote.remoteMethod("resumeProducing")
self.pauseProducing = remote.remoteMethod("pauseProducing")
self.stopProducing = remote.remoteMethod("stopProducing")
class Session(components.Componentized):
"""
A user's session with a system.
This utility class contains no functionality, but is used to
represent a session.
@ivar site: The L{Site} that generated the session.
@type site: L{Site}
@ivar uid: A unique identifier for the session.
@type uid: L{bytes}
@ivar _reactor: An object providing L{IReactorTime} to use for scheduling
expiration.
@ivar sessionTimeout: Time after last modification the session will expire,
in seconds.
@type sessionTimeout: L{float}
@ivar lastModified: Time the C{touch()} method was last called (or time the
session was created). A UNIX timestamp as returned by
L{IReactorTime.seconds()}.
@type lastModified: L{float}
"""
sessionTimeout = 900
_expireCall = None
def __init__(self, site, uid, reactor=None):
"""
Initialize a session with a unique ID for that session.
@param reactor: L{IReactorTime} used to schedule expiration of the
session. If C{None}, the reactor associated with I{site} is used.
"""
super().__init__()
if reactor is None:
reactor = site.reactor
self._reactor = reactor
self.site = site
self.uid = uid
self.expireCallbacks = []
self.touch()
self.sessionNamespaces = {}
def startCheckingExpiration(self):
"""
Start expiration tracking.
@return: L{None}
"""
self._expireCall = self._reactor.callLater(self.sessionTimeout, self.expire)
def notifyOnExpire(self, callback):
"""
Call this callback when the session expires or logs out.
"""
self.expireCallbacks.append(callback)
def expire(self):
"""
Expire/logout of the session.
"""
del self.site.sessions[self.uid]
for c in self.expireCallbacks:
c()
self.expireCallbacks = []
if self._expireCall and self._expireCall.active():
self._expireCall.cancel()
# Break reference cycle.
self._expireCall = None
def touch(self):
"""
Mark the session as modified, which resets expiration timer.
"""
self.lastModified = self._reactor.seconds()
if self._expireCall is not None:
self._expireCall.reset(self.sessionTimeout)
version = networkString(f"TwistedWeb/{copyright.version}")
@implementer(interfaces.IProtocolNegotiationFactory)
class Site(HTTPFactory):
"""
A web site: manage log, sessions, and resources.
@ivar requestFactory: A factory which is called with (channel)
and creates L{Request} instances. Default to L{Request}.
@ivar displayTracebacks: If set, unhandled exceptions raised during
rendering are returned to the client as HTML. Default to C{False}.
@ivar sessionFactory: factory for sessions objects. Default to L{Session}.
@ivar sessions: Mapping of session IDs to objects returned by
C{sessionFactory}.
@type sessions: L{dict} mapping L{bytes} to L{Session} given the default
C{sessionFactory}
@ivar counter: The number of sessions that have been generated.
@type counter: L{int}
@ivar sessionCheckTime: Deprecated and unused. See
L{Session.sessionTimeout} instead.
"""
counter = 0
requestFactory = Request
displayTracebacks = False
sessionFactory = Session
sessionCheckTime = 1800
_entropy = os.urandom
def __init__(self, resource, requestFactory=None, *args, **kwargs):
"""
@param resource: The root of the resource hierarchy. All request
traversal for requests received by this factory will begin at this
resource.
@type resource: L{IResource} provider
@param requestFactory: Overwrite for default requestFactory.
@type requestFactory: C{callable} or C{class}.
@see: L{twisted.web.http.HTTPFactory.__init__}
"""
super().__init__(*args, **kwargs)
self.sessions = {}
self.resource = resource
if requestFactory is not None:
self.requestFactory = requestFactory
def _openLogFile(self, path):
from twisted.python import logfile
return logfile.LogFile(os.path.basename(path), os.path.dirname(path))
def __getstate__(self):
d = self.__dict__.copy()
d["sessions"] = {}
return d
def _mkuid(self):
"""
(internal) Generate an opaque, unique ID for a user's session.
"""
self.counter = self.counter + 1
return hexlify(self._entropy(32))
def makeSession(self):
"""
Generate a new Session instance, and store it for future reference.
"""
uid = self._mkuid()
session = self.sessions[uid] = self.sessionFactory(self, uid)
session.startCheckingExpiration()
return session
def getSession(self, uid):
"""
Get a previously generated session.
@param uid: Unique ID of the session.
@type uid: L{bytes}.
@raise KeyError: If the session is not found.
"""
return self.sessions[uid]
def buildProtocol(self, addr):
"""
Generate a channel attached to this site.
"""
channel = super().buildProtocol(addr)
channel.requestFactory = self.requestFactory
channel.site = self
return channel
isLeaf = 0
def render(self, request):
"""
Redirect because a Site is always a directory.
"""
request.redirect(request.prePathURL() + b"/")
request.finish()
def getChildWithDefault(self, pathEl, request):
"""
Emulate a resource's getChild method.
"""
request.site = self
return self.resource.getChildWithDefault(pathEl, request)
def getResourceFor(self, request):
"""
Get a resource for a request.
This iterates through the resource hierarchy, calling
getChildWithDefault on each resource it finds for a path element,
stopping when it hits an element where isLeaf is true.
"""
request.site = self
# Sitepath is used to determine cookie names between distributed
# servers and disconnected sites.
request.sitepath = copy.copy(request.prepath)
return resource.getChildForRequest(self.resource, request)
# IProtocolNegotiationFactory
def acceptableProtocols(self):
"""
Protocols this server can speak.
"""
baseProtocols = [b"http/1.1"]
if http.H2_ENABLED:
baseProtocols.insert(0, b"h2")
return baseProtocols

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,644 @@
# -*- test-case-name: twisted.web.test.test_xml -*-
#
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
*S*mall, *U*ncomplicated *X*ML.
This is a very simple implementation of XML/HTML as a network
protocol. It is not at all clever. Its main features are that it
does not:
- support namespaces
- mung mnemonic entity references
- validate
- perform *any* external actions (such as fetching URLs or writing files)
under *any* circumstances
- has lots and lots of horrible hacks for supporting broken HTML (as an
option, they're not on by default).
"""
from twisted.internet.protocol import Protocol
from twisted.python.reflect import prefixedMethodNames
# Elements of the three-tuples in the state table.
BEGIN_HANDLER = 0
DO_HANDLER = 1
END_HANDLER = 2
identChars = ".-_:"
lenientIdentChars = identChars + ";+#/%~"
def nop(*args, **kw):
"Do nothing."
def unionlist(*args):
l = []
for x in args:
l.extend(x)
d = {x: 1 for x in l}
return d.keys()
def zipfndict(*args, **kw):
default = kw.get("default", nop)
d = {}
for key in unionlist(*(fndict.keys() for fndict in args)):
d[key] = tuple(x.get(key, default) for x in args)
return d
def prefixedMethodClassDict(clazz, prefix):
return {
name: getattr(clazz, prefix + name)
for name in prefixedMethodNames(clazz, prefix)
}
def prefixedMethodObjDict(obj, prefix):
return {
name: getattr(obj, prefix + name)
for name in prefixedMethodNames(obj.__class__, prefix)
}
class ParseError(Exception):
def __init__(self, filename, line, col, message):
self.filename = filename
self.line = line
self.col = col
self.message = message
def __str__(self) -> str:
return f"{self.filename}:{self.line}:{self.col}: {self.message}"
class XMLParser(Protocol):
state = None
encodings = None
filename = "<xml />"
beExtremelyLenient = 0
_prepend = None
# _leadingBodyData will sometimes be set before switching to the
# 'bodydata' state, when we "accidentally" read a byte of bodydata
# in a different state.
_leadingBodyData = None
def connectionMade(self):
self.lineno = 1
self.colno = 0
self.encodings = []
def saveMark(self):
"""Get the line number and column of the last character parsed"""
# This gets replaced during dataReceived, restored afterwards
return (self.lineno, self.colno)
def _parseError(self, message):
raise ParseError(*((self.filename,) + self.saveMark() + (message,)))
def _buildStateTable(self):
"""Return a dictionary of begin, do, end state function tuples"""
# _buildStateTable leaves something to be desired but it does what it
# does.. probably slowly, so I'm doing some evil caching so it doesn't
# get called more than once per class.
stateTable = getattr(self.__class__, "__stateTable", None)
if stateTable is None:
stateTable = self.__class__.__stateTable = zipfndict(
*(
prefixedMethodObjDict(self, prefix)
for prefix in ("begin_", "do_", "end_")
)
)
return stateTable
def _decode(self, data):
if "UTF-16" in self.encodings or "UCS-2" in self.encodings:
assert not len(data) & 1, "UTF-16 must come in pairs for now"
if self._prepend:
data = self._prepend + data
for encoding in self.encodings:
data = str(data, encoding)
return data
def maybeBodyData(self):
if self.endtag:
return "bodydata"
# Get ready for fun! We're going to allow
# <script>if (foo < bar)</script> to work!
# We do this by making everything between <script> and
# </script> a Text
# BUT <script src="foo"> will be special-cased to do regular,
# lenient behavior, because those may not have </script>
# -radix
if self.tagName == "script" and "src" not in self.tagAttributes:
# we do this ourselves rather than having begin_waitforendscript
# because that can get called multiple times and we don't want
# bodydata to get reset other than the first time.
self.begin_bodydata(None)
return "waitforendscript"
return "bodydata"
def dataReceived(self, data):
stateTable = self._buildStateTable()
if not self.state:
# all UTF-16 starts with this string
if data.startswith((b"\xff\xfe", b"\xfe\xff")):
self._prepend = data[0:2]
self.encodings.append("UTF-16")
data = data[2:]
self.state = "begin"
if self.encodings:
data = self._decode(data)
else:
data = data.decode("utf-8")
# bring state, lineno, colno into local scope
lineno, colno = self.lineno, self.colno
curState = self.state
# replace saveMark with a nested scope function
_saveMark = self.saveMark
def saveMark():
return (lineno, colno)
self.saveMark = saveMark
# fetch functions from the stateTable
beginFn, doFn, endFn = stateTable[curState]
try:
for byte in data:
# do newline stuff
if byte == "\n":
lineno += 1
colno = 0
else:
colno += 1
newState = doFn(byte)
if newState is not None and newState != curState:
# this is the endFn from the previous state
endFn()
curState = newState
beginFn, doFn, endFn = stateTable[curState]
beginFn(byte)
finally:
self.saveMark = _saveMark
self.lineno, self.colno = lineno, colno
# state doesn't make sense if there's an exception..
self.state = curState
def connectionLost(self, reason):
"""
End the last state we were in.
"""
stateTable = self._buildStateTable()
stateTable[self.state][END_HANDLER]()
# state methods
def do_begin(self, byte):
if byte.isspace():
return
if byte != "<":
if self.beExtremelyLenient:
self._leadingBodyData = byte
return "bodydata"
self._parseError(f"First char of document [{byte!r}] wasn't <")
return "tagstart"
def begin_comment(self, byte):
self.commentbuf = ""
def do_comment(self, byte):
self.commentbuf += byte
if self.commentbuf.endswith("-->"):
self.gotComment(self.commentbuf[:-3])
return "bodydata"
def begin_tagstart(self, byte):
self.tagName = "" # name of the tag
self.tagAttributes = {} # attributes of the tag
self.termtag = 0 # is the tag self-terminating
self.endtag = 0
def do_tagstart(self, byte):
if byte.isalnum() or byte in identChars:
self.tagName += byte
if self.tagName == "!--":
return "comment"
elif byte.isspace():
if self.tagName:
if self.endtag:
# properly strict thing to do here is probably to only
# accept whitespace
return "waitforgt"
return "attrs"
else:
self._parseError("Whitespace before tag-name")
elif byte == ">":
if self.endtag:
self.gotTagEnd(self.tagName)
return "bodydata"
else:
self.gotTagStart(self.tagName, {})
return (
(not self.beExtremelyLenient) and "bodydata" or self.maybeBodyData()
)
elif byte == "/":
if self.tagName:
return "afterslash"
else:
self.endtag = 1
elif byte in "!?":
if self.tagName:
if not self.beExtremelyLenient:
self._parseError("Invalid character in tag-name")
else:
self.tagName += byte
self.termtag = 1
elif byte == "[":
if self.tagName == "!":
return "expectcdata"
else:
self._parseError("Invalid '[' in tag-name")
else:
if self.beExtremelyLenient:
self.bodydata = "<"
return "unentity"
self._parseError("Invalid tag character: %r" % byte)
def begin_unentity(self, byte):
self.bodydata += byte
def do_unentity(self, byte):
self.bodydata += byte
return "bodydata"
def end_unentity(self):
self.gotText(self.bodydata)
def begin_expectcdata(self, byte):
self.cdatabuf = byte
def do_expectcdata(self, byte):
self.cdatabuf += byte
cdb = self.cdatabuf
cd = "[CDATA["
if len(cd) > len(cdb):
if cd.startswith(cdb):
return
elif self.beExtremelyLenient:
## WHAT THE CRAP!? MSWord9 generates HTML that includes these
## bizarre <![if !foo]> <![endif]> chunks, so I've gotta ignore
## 'em as best I can. this should really be a separate parse
## state but I don't even have any idea what these _are_.
return "waitforgt"
else:
self._parseError("Mal-formed CDATA header")
if cd == cdb:
self.cdatabuf = ""
return "cdata"
self._parseError("Mal-formed CDATA header")
def do_cdata(self, byte):
self.cdatabuf += byte
if self.cdatabuf.endswith("]]>"):
self.cdatabuf = self.cdatabuf[:-3]
return "bodydata"
def end_cdata(self):
self.gotCData(self.cdatabuf)
self.cdatabuf = ""
def do_attrs(self, byte):
if byte.isalnum() or byte in identChars:
# XXX FIXME really handle !DOCTYPE at some point
if self.tagName == "!DOCTYPE":
return "doctype"
if self.tagName[0] in "!?":
return "waitforgt"
return "attrname"
elif byte.isspace():
return
elif byte == ">":
self.gotTagStart(self.tagName, self.tagAttributes)
return (not self.beExtremelyLenient) and "bodydata" or self.maybeBodyData()
elif byte == "/":
return "afterslash"
elif self.beExtremelyLenient:
# discard and move on? Only case I've seen of this so far was:
# <foo bar="baz"">
return
self._parseError("Unexpected character: %r" % byte)
def begin_doctype(self, byte):
self.doctype = byte
def do_doctype(self, byte):
if byte == ">":
return "bodydata"
self.doctype += byte
def end_doctype(self):
self.gotDoctype(self.doctype)
self.doctype = None
def do_waitforgt(self, byte):
if byte == ">":
if self.endtag or not self.beExtremelyLenient:
return "bodydata"
return self.maybeBodyData()
def begin_attrname(self, byte):
self.attrname = byte
self._attrname_termtag = 0
def do_attrname(self, byte):
if byte.isalnum() or byte in identChars:
self.attrname += byte
return
elif byte == "=":
return "beforeattrval"
elif byte.isspace():
return "beforeeq"
elif self.beExtremelyLenient:
if byte in "\"'":
return "attrval"
if byte in lenientIdentChars or byte.isalnum():
self.attrname += byte
return
if byte == "/":
self._attrname_termtag = 1
return
if byte == ">":
self.attrval = "True"
self.tagAttributes[self.attrname] = self.attrval
self.gotTagStart(self.tagName, self.tagAttributes)
if self._attrname_termtag:
self.gotTagEnd(self.tagName)
return "bodydata"
return self.maybeBodyData()
# something is really broken. let's leave this attribute where it
# is and move on to the next thing
return
self._parseError(f"Invalid attribute name: {self.attrname!r} {byte!r}")
def do_beforeattrval(self, byte):
if byte in "\"'":
return "attrval"
elif byte.isspace():
return
elif self.beExtremelyLenient:
if byte in lenientIdentChars or byte.isalnum():
return "messyattr"
if byte == ">":
self.attrval = "True"
self.tagAttributes[self.attrname] = self.attrval
self.gotTagStart(self.tagName, self.tagAttributes)
return self.maybeBodyData()
if byte == "\\":
# I saw this in actual HTML once:
# <font size=\"3\"><sup>SM</sup></font>
return
self._parseError(
"Invalid initial attribute value: %r; Attribute values must be quoted."
% byte
)
attrname = ""
attrval = ""
def begin_beforeeq(self, byte):
self._beforeeq_termtag = 0
def do_beforeeq(self, byte):
if byte == "=":
return "beforeattrval"
elif byte.isspace():
return
elif self.beExtremelyLenient:
if byte.isalnum() or byte in identChars:
self.attrval = "True"
self.tagAttributes[self.attrname] = self.attrval
return "attrname"
elif byte == ">":
self.attrval = "True"
self.tagAttributes[self.attrname] = self.attrval
self.gotTagStart(self.tagName, self.tagAttributes)
if self._beforeeq_termtag:
self.gotTagEnd(self.tagName)
return "bodydata"
return self.maybeBodyData()
elif byte == "/":
self._beforeeq_termtag = 1
return
self._parseError("Invalid attribute")
def begin_attrval(self, byte):
self.quotetype = byte
self.attrval = ""
def do_attrval(self, byte):
if byte == self.quotetype:
return "attrs"
self.attrval += byte
def end_attrval(self):
self.tagAttributes[self.attrname] = self.attrval
self.attrname = self.attrval = ""
def begin_messyattr(self, byte):
self.attrval = byte
def do_messyattr(self, byte):
if byte.isspace():
return "attrs"
elif byte == ">":
endTag = 0
if self.attrval.endswith("/"):
endTag = 1
self.attrval = self.attrval[:-1]
self.tagAttributes[self.attrname] = self.attrval
self.gotTagStart(self.tagName, self.tagAttributes)
if endTag:
self.gotTagEnd(self.tagName)
return "bodydata"
return self.maybeBodyData()
else:
self.attrval += byte
def end_messyattr(self):
if self.attrval:
self.tagAttributes[self.attrname] = self.attrval
def begin_afterslash(self, byte):
self._after_slash_closed = 0
def do_afterslash(self, byte):
# this state is only after a self-terminating slash, e.g. <foo/>
if self._after_slash_closed:
self._parseError("Mal-formed") # XXX When does this happen??
if byte != ">":
if self.beExtremelyLenient:
return
else:
self._parseError("No data allowed after '/'")
self._after_slash_closed = 1
self.gotTagStart(self.tagName, self.tagAttributes)
self.gotTagEnd(self.tagName)
# don't need maybeBodyData here because there better not be
# any javascript code after a <script/>... we'll see :(
return "bodydata"
def begin_bodydata(self, byte):
if self._leadingBodyData:
self.bodydata = self._leadingBodyData
del self._leadingBodyData
else:
self.bodydata = ""
def do_bodydata(self, byte):
if byte == "<":
return "tagstart"
if byte == "&":
return "entityref"
self.bodydata += byte
def end_bodydata(self):
self.gotText(self.bodydata)
self.bodydata = ""
def do_waitforendscript(self, byte):
if byte == "<":
return "waitscriptendtag"
self.bodydata += byte
def begin_waitscriptendtag(self, byte):
self.temptagdata = ""
self.tagName = ""
self.endtag = 0
def do_waitscriptendtag(self, byte):
# 1 enforce / as first byte read
# 2 enforce following bytes to be subset of "script" until
# tagName == "script"
# 2a when that happens, gotText(self.bodydata) and gotTagEnd(self.tagName)
# 3 spaces can happen anywhere, they're ignored
# e.g. < / script >
# 4 anything else causes all data I've read to be moved to the
# bodydata, and switch back to waitforendscript state
# If it turns out this _isn't_ a </script>, we need to
# remember all the data we've been through so we can append it
# to bodydata
self.temptagdata += byte
# 1
if byte == "/":
self.endtag = True
elif not self.endtag:
self.bodydata += "<" + self.temptagdata
return "waitforendscript"
# 2
elif byte.isalnum() or byte in identChars:
self.tagName += byte
if not "script".startswith(self.tagName):
self.bodydata += "<" + self.temptagdata
return "waitforendscript"
elif self.tagName == "script":
self.gotText(self.bodydata)
self.gotTagEnd(self.tagName)
return "waitforgt"
# 3
elif byte.isspace():
return "waitscriptendtag"
# 4
else:
self.bodydata += "<" + self.temptagdata
return "waitforendscript"
def begin_entityref(self, byte):
self.erefbuf = ""
self.erefextra = "" # extra bit for lenient mode
def do_entityref(self, byte):
if byte.isspace() or byte == "<":
if self.beExtremelyLenient:
# '&foo' probably was '&amp;foo'
if self.erefbuf and self.erefbuf != "amp":
self.erefextra = self.erefbuf
self.erefbuf = "amp"
if byte == "<":
return "tagstart"
else:
self.erefextra += byte
return "spacebodydata"
self._parseError("Bad entity reference")
elif byte != ";":
self.erefbuf += byte
else:
return "bodydata"
def end_entityref(self):
self.gotEntityReference(self.erefbuf)
# hacky support for space after & in entityref in beExtremelyLenient
# state should only happen in that case
def begin_spacebodydata(self, byte):
self.bodydata = self.erefextra
self.erefextra = None
do_spacebodydata = do_bodydata
end_spacebodydata = end_bodydata
# Sorta SAX-ish API
def gotTagStart(self, name, attributes):
"""Encountered an opening tag.
Default behaviour is to print."""
print("begin", name, attributes)
def gotText(self, data):
"""Encountered text
Default behaviour is to print."""
print("text:", repr(data))
def gotEntityReference(self, entityRef):
"""Encountered mnemonic entity reference
Default behaviour is to print."""
print("entityRef: &%s;" % entityRef)
def gotComment(self, comment):
"""Encountered comment.
Default behaviour is to ignore."""
pass
def gotCData(self, cdata):
"""Encountered CDATA
Default behaviour is to call the gotText method"""
self.gotText(cdata)
def gotDoctype(self, doctype):
"""Encountered DOCTYPE
This is really grotty: it basically just gives you everything between
'<!DOCTYPE' and '>' as an argument.
"""
print("!DOCTYPE", repr(doctype))
def gotTagEnd(self, name):
"""Encountered closing tag
Default behaviour is to print."""
print("end", name)

View File

@@ -0,0 +1,322 @@
# -*- test-case-name: twisted.web.test.test_tap -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Support for creating a service which runs a web server.
"""
import os
import warnings
import incremental
from twisted.application import service, strports
from twisted.internet import interfaces, reactor
from twisted.python import deprecate, reflect, threadpool, usage
from twisted.spread import pb
from twisted.web import demo, distrib, resource, script, server, static, twcgi, wsgi
class Options(usage.Options):
"""
Define the options accepted by the I{twistd web} plugin.
"""
synopsis = "[web options]"
optParameters = [
["logfile", "l", None, "Path to web CLF (Combined Log Format) log file."],
[
"certificate",
"c",
"server.pem",
"(DEPRECATED: use --listen) " "SSL certificate to use for HTTPS. ",
],
[
"privkey",
"k",
"server.pem",
"(DEPRECATED: use --listen) " "SSL certificate to use for HTTPS.",
],
]
optFlags = [
[
"notracebacks",
"n",
(
"(DEPRECATED: Tracebacks are disabled by default. "
"See --enable-tracebacks to turn them on."
),
],
[
"display-tracebacks",
"",
(
"Show uncaught exceptions during rendering tracebacks to "
"the client. WARNING: This may be a security risk and "
"expose private data!"
),
],
]
optFlags.append(
[
"personal",
"",
"Instead of generating a webserver, generate a "
"ResourcePublisher which listens on the port given by "
"--listen, or ~/%s " % (distrib.UserDirectory.userSocketName,)
+ "if --listen is not specified.",
]
)
compData = usage.Completions(
optActions={
"logfile": usage.CompleteFiles("*.log"),
"certificate": usage.CompleteFiles("*.pem"),
"privkey": usage.CompleteFiles("*.pem"),
}
)
longdesc = """\
This starts a webserver. If you specify no arguments, it will be a
demo webserver that has the Test class from twisted.web.demo in it."""
def __init__(self):
usage.Options.__init__(self)
self["indexes"] = []
self["root"] = None
self["extraHeaders"] = []
self["ports"] = []
self["port"] = self["https"] = None
def opt_port(self, port):
"""
(DEPRECATED: use --listen)
Strports description of port to start the server on
"""
msg = deprecate.getDeprecationWarningString(
self.opt_port, incremental.Version("Twisted", 18, 4, 0)
)
warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
self["port"] = port
opt_p = opt_port
def opt_https(self, port):
"""
(DEPRECATED: use --listen)
Port to listen on for Secure HTTP.
"""
msg = deprecate.getDeprecationWarningString(
self.opt_https, incremental.Version("Twisted", 18, 4, 0)
)
warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
self["https"] = port
def opt_listen(self, port):
"""
Add an strports description of port to start the server on.
[default: tcp:8080]
"""
self["ports"].append(port)
def opt_index(self, indexName):
"""
Add the name of a file used to check for directory indexes.
[default: index, index.html]
"""
self["indexes"].append(indexName)
opt_i = opt_index
def opt_user(self):
"""
Makes a server with ~/public_html and ~/.twistd-web-pb support for
users.
"""
self["root"] = distrib.UserDirectory()
opt_u = opt_user
def opt_path(self, path):
"""
<path> is either a specific file or a directory to be set as the root
of the web server. Use this if you have a directory full of HTML, cgi,
epy, or rpy files or any other files that you want to be served up raw.
"""
self["root"] = static.File(os.path.abspath(path))
self["root"].processors = {
".epy": script.PythonScript,
".rpy": script.ResourceScript,
}
self["root"].processors[".cgi"] = twcgi.CGIScript
def opt_processor(self, proc):
"""
`ext=class' where `class' is added as a Processor for files ending
with `ext'.
"""
if not isinstance(self["root"], static.File):
raise usage.UsageError("You can only use --processor after --path.")
ext, klass = proc.split("=", 1)
self["root"].processors[ext] = reflect.namedClass(klass)
def opt_class(self, className):
"""
Create a Resource subclass with a zero-argument constructor.
"""
classObj = reflect.namedClass(className)
self["root"] = classObj()
def opt_resource_script(self, name):
"""
An .rpy file to be used as the root resource of the webserver.
"""
self["root"] = script.ResourceScriptWrapper(name)
def opt_wsgi(self, name):
"""
The FQPN of a WSGI application object to serve as the root resource of
the webserver.
"""
try:
application = reflect.namedAny(name)
except (AttributeError, ValueError):
raise usage.UsageError(f"No such WSGI application: {name!r}")
pool = threadpool.ThreadPool()
reactor.callWhenRunning(pool.start)
reactor.addSystemEventTrigger("after", "shutdown", pool.stop)
self["root"] = wsgi.WSGIResource(reactor, pool, application)
def opt_mime_type(self, defaultType):
"""
Specify the default mime-type for static files.
"""
if not isinstance(self["root"], static.File):
raise usage.UsageError("You can only use --mime_type after --path.")
self["root"].defaultType = defaultType
opt_m = opt_mime_type
def opt_allow_ignore_ext(self):
"""
Specify whether or not a request for 'foo' should return 'foo.ext'
"""
if not isinstance(self["root"], static.File):
raise usage.UsageError(
"You can only use --allow_ignore_ext " "after --path."
)
self["root"].ignoreExt("*")
def opt_ignore_ext(self, ext):
"""
Specify an extension to ignore. These will be processed in order.
"""
if not isinstance(self["root"], static.File):
raise usage.UsageError("You can only use --ignore_ext " "after --path.")
self["root"].ignoreExt(ext)
def opt_add_header(self, header):
"""
Specify an additional header to be included in all responses. Specified
as "HeaderName: HeaderValue".
"""
name, value = header.split(":", 1)
self["extraHeaders"].append((name.strip(), value.strip()))
def postOptions(self):
"""
Set up conditional defaults and check for dependencies.
If SSL is not available but an HTTPS server was configured, raise a
L{UsageError} indicating that this is not possible.
If no server port was supplied, select a default appropriate for the
other options supplied.
"""
if self["port"] is not None:
self["ports"].append(self["port"])
if self["https"] is not None:
try:
reflect.namedModule("OpenSSL.SSL")
except ImportError:
raise usage.UsageError("SSL support not installed")
sslStrport = "ssl:port={}:privateKey={}:certKey={}".format(
self["https"],
self["privkey"],
self["certificate"],
)
self["ports"].append(sslStrport)
if len(self["ports"]) == 0:
if self["personal"]:
path = os.path.expanduser(
os.path.join("~", distrib.UserDirectory.userSocketName)
)
self["ports"].append("unix:" + path)
else:
self["ports"].append("tcp:8080")
def makePersonalServerFactory(site):
"""
Create and return a factory which will respond to I{distrib} requests
against the given site.
@type site: L{twisted.web.server.Site}
@rtype: L{twisted.internet.protocol.Factory}
"""
return pb.PBServerFactory(distrib.ResourcePublisher(site))
class _AddHeadersResource(resource.Resource):
def __init__(self, originalResource, headers):
self._originalResource = originalResource
self._headers = headers
def getChildWithDefault(self, name, request):
for k, v in self._headers:
request.responseHeaders.addRawHeader(k, v)
return self._originalResource.getChildWithDefault(name, request)
def makeService(config):
s = service.MultiService()
if config["root"]:
root = config["root"]
if config["indexes"]:
config["root"].indexNames = config["indexes"]
else:
# This really ought to be web.Admin or something
root = demo.Test()
if isinstance(root, static.File):
root.registry.setComponent(interfaces.IServiceCollection, s)
if config["extraHeaders"]:
root = _AddHeadersResource(root, config["extraHeaders"])
if config["logfile"]:
site = server.Site(root, logPath=config["logfile"])
else:
site = server.Site(root)
if config["display-tracebacks"]:
site.displayTracebacks = True
# Deprecate --notracebacks/-n
if config["notracebacks"]:
msg = deprecate._getDeprecationWarningString(
"--notracebacks", incremental.Version("Twisted", 19, 7, 0)
)
warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
if config["personal"]:
site = makePersonalServerFactory(site)
for port in config["ports"]:
svc = strports.service(port, site)
svc.setServiceParent(s)
return s

View File

@@ -0,0 +1,60 @@
# -*- test-case-name: twisted.web.test.test_template -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
HTML rendering for twisted.web.
@var VALID_HTML_TAG_NAMES: A list of recognized HTML tag names, used by the
L{tag} object.
@var TEMPLATE_NAMESPACE: The XML namespace used to identify attributes and
elements used by the templating system, which should be removed from the
final output document.
@var tags: A convenience object which can produce L{Tag} objects on demand via
attribute access. For example: C{tags.div} is equivalent to C{Tag("div")}.
Tags not specified in L{VALID_HTML_TAG_NAMES} will result in an
L{AttributeError}.
"""
__all__ = [
"TEMPLATE_NAMESPACE",
"VALID_HTML_TAG_NAMES",
"Element",
"Flattenable",
"TagLoader",
"XMLString",
"XMLFile",
"renderer",
"flatten",
"flattenString",
"tags",
"Comment",
"CDATA",
"Tag",
"slot",
"CharRef",
"renderElement",
]
from ._stan import CharRef
from ._template_util import (
CDATA,
TEMPLATE_NAMESPACE,
VALID_HTML_TAG_NAMES,
Comment,
Element,
Flattenable,
Tag,
TagLoader,
XMLFile,
XMLString,
flatten,
flattenString,
renderElement,
renderer,
slot,
tags,
)

View File

@@ -0,0 +1,6 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for L{twisted.web}.
"""

Some files were not shown because too many files have changed in this diff Show More