17.12
This commit is contained in:
@@ -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
|
||||
"""
|
||||
@@ -0,0 +1,421 @@
|
||||
# -*- test-case-name: twisted.web.test.test_flatten -*-
|
||||
# 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 division, absolute_import
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from sys import exc_info
|
||||
from types import GeneratorType
|
||||
from traceback import extract_tb
|
||||
|
||||
try:
|
||||
from inspect import iscoroutine
|
||||
except ImportError:
|
||||
def iscoroutine(*args, **kwargs):
|
||||
return False
|
||||
|
||||
from twisted.python.compat import unicode, nativeString, iteritems
|
||||
from twisted.internet.defer import Deferred, ensureDeferred
|
||||
from twisted.web._stan import Tag, slot, voidElements, Comment, CDATA, CharRef
|
||||
from twisted.web.error import UnfilledSlot, UnsupportedType, FlattenerError
|
||||
from twisted.web.iweb import IRenderable
|
||||
|
||||
|
||||
|
||||
def escapeForContent(data):
|
||||
"""
|
||||
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{&<>}).
|
||||
|
||||
This is used as an input to L{_flattenElement}'s C{dataEscaper} parameter.
|
||||
|
||||
@type data: C{bytes} or C{unicode}
|
||||
@param data: The string to escape.
|
||||
|
||||
@rtype: C{bytes}
|
||||
@return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
|
||||
encoded string.
|
||||
"""
|
||||
if isinstance(data, unicode):
|
||||
data = data.encode('utf-8')
|
||||
data = data.replace(b'&', b'&'
|
||||
).replace(b'<', b'<'
|
||||
).replace(b'>', b'>')
|
||||
return data
|
||||
|
||||
|
||||
|
||||
def attributeEscapingDoneOutside(data):
|
||||
"""
|
||||
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.
|
||||
|
||||
@type data: C{bytes} or C{unicode}
|
||||
@param data: The string to escape.
|
||||
|
||||
@return: The string, unchanged, except for encoding.
|
||||
@rtype: C{bytes}
|
||||
"""
|
||||
if isinstance(data, unicode):
|
||||
return data.encode("utf-8")
|
||||
return data
|
||||
|
||||
|
||||
|
||||
def writeWithAttributeEscaping(write):
|
||||
"""
|
||||
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>&</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="<x />"
|
||||
/>}. 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="<-- comment -->" />}. 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):
|
||||
write(escapeForContent(data).replace(b'"', b'"'))
|
||||
return _write
|
||||
|
||||
|
||||
|
||||
def escapedCDATA(data):
|
||||
"""
|
||||
Escape CDATA for inclusion in a document.
|
||||
|
||||
@type data: L{str} or L{unicode}
|
||||
@param data: The string to escape.
|
||||
|
||||
@rtype: L{str}
|
||||
@return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
|
||||
encoded string.
|
||||
"""
|
||||
if isinstance(data, unicode):
|
||||
data = data.encode('utf-8')
|
||||
return data.replace(b']]>', b']]]]><![CDATA[>')
|
||||
|
||||
|
||||
|
||||
def escapedComment(data):
|
||||
"""
|
||||
Escape a comment for inclusion in a document.
|
||||
|
||||
@type data: L{str} or L{unicode}
|
||||
@param data: The string to escape.
|
||||
|
||||
@rtype: C{str}
|
||||
@return: The quoted form of C{data}. If C{data} is unicode, return a utf-8
|
||||
encoded string.
|
||||
"""
|
||||
if isinstance(data, unicode):
|
||||
data = data.encode('utf-8')
|
||||
data = data.replace(b'--', b'- - ').replace(b'>', b'>')
|
||||
if data and data[-1:] == b'-':
|
||||
data += b' '
|
||||
return data
|
||||
|
||||
|
||||
|
||||
def _getSlotValue(name, slotData, default=None):
|
||||
"""
|
||||
Find the value of the named slot in the given stack of slot data.
|
||||
"""
|
||||
for slotFrame in slotData[::-1]:
|
||||
if slotFrame is not None and name in slotFrame:
|
||||
return slotFrame[name]
|
||||
else:
|
||||
if default is not None:
|
||||
return default
|
||||
raise UnfilledSlot(name)
|
||||
|
||||
|
||||
|
||||
def _flattenElement(request, root, write, slotData, renderFactory,
|
||||
dataEscaper):
|
||||
"""
|
||||
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 yields L{bytes} that should be written
|
||||
to the output. However it may also 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
|
||||
either be L{bytes}, in which case it's written, or another generator,
|
||||
in which case it is iterated. See L{_flattenTree} for the trampoline
|
||||
that consumes said values.
|
||||
@rtype: An iterator which yields L{bytes}, L{Deferred}, and more iterators
|
||||
of the same type.
|
||||
"""
|
||||
def keepGoing(newRoot, dataEscaper=dataEscaper,
|
||||
renderFactory=renderFactory, write=write):
|
||||
return _flattenElement(request, newRoot, write, slotData,
|
||||
renderFactory, dataEscaper)
|
||||
if isinstance(root, (bytes, unicode)):
|
||||
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)
|
||||
if root.render is not None:
|
||||
rendererName = root.render
|
||||
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, unicode):
|
||||
tagName = root.tagName.encode('ascii')
|
||||
else:
|
||||
tagName = root.tagName
|
||||
write(tagName)
|
||||
for k, v in iteritems(root.attributes):
|
||||
if isinstance(k, unicode):
|
||||
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 root.addCallback(lambda result: (result, keepGoing(result)))
|
||||
elif iscoroutine(root):
|
||||
d = ensureDeferred(root)
|
||||
yield d.addCallback(lambda result: (result, keepGoing(result)))
|
||||
elif IRenderable.providedBy(root):
|
||||
result = root.render(request)
|
||||
yield keepGoing(result, renderFactory=root)
|
||||
else:
|
||||
raise UnsupportedType(root)
|
||||
|
||||
|
||||
|
||||
def _flattenTree(request, root, write):
|
||||
"""
|
||||
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: An iterator which yields objects of type L{bytes} and L{Deferred}.
|
||||
A L{Deferred} is only yielded when one is encountered in the process of
|
||||
flattening C{root}. The returned iterator must not be iterated again
|
||||
until the L{Deferred} is called back.
|
||||
"""
|
||||
stack = [_flattenElement(request, root, write, [], None, escapeForContent)]
|
||||
while stack:
|
||||
try:
|
||||
frame = stack[-1].gi_frame
|
||||
element = next(stack[-1])
|
||||
except StopIteration:
|
||||
stack.pop()
|
||||
except Exception as e:
|
||||
stack.pop()
|
||||
roots = []
|
||||
for generator in stack:
|
||||
roots.append(generator.gi_frame.f_locals['root'])
|
||||
roots.append(frame.f_locals['root'])
|
||||
raise FlattenerError(e, roots, extract_tb(exc_info()[2]))
|
||||
else:
|
||||
if isinstance(element, Deferred):
|
||||
def cbx(originalAndToFlatten):
|
||||
original, toFlatten = originalAndToFlatten
|
||||
stack.append(toFlatten)
|
||||
return original
|
||||
yield element.addCallback(cbx)
|
||||
else:
|
||||
stack.append(element)
|
||||
|
||||
|
||||
def _writeFlattenedData(state, write, result):
|
||||
"""
|
||||
Take strings from an iterator and pass them to a writer function.
|
||||
|
||||
@param state: An iterator of L{str} and L{Deferred}. L{str} instances will
|
||||
be passed to C{write}. L{Deferred} instances will be waited on before
|
||||
resuming iteration of C{state}.
|
||||
|
||||
@param write: A callable which will be invoked with each L{str}
|
||||
produced by iterating C{state}.
|
||||
|
||||
@param result: A L{Deferred} which will be called back when C{state} has
|
||||
been completely flattened into C{write} or which will be errbacked if
|
||||
an exception in a generator passed to C{state} or an errback from a
|
||||
L{Deferred} from state occurs.
|
||||
|
||||
@return: L{None}
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
element = next(state)
|
||||
except StopIteration:
|
||||
result.callback(None)
|
||||
except:
|
||||
result.errback()
|
||||
else:
|
||||
def cby(original):
|
||||
_writeFlattenedData(state, write, result)
|
||||
return original
|
||||
element.addCallbacks(cby, result.errback)
|
||||
break
|
||||
|
||||
|
||||
|
||||
def flatten(request, root, write):
|
||||
"""
|
||||
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{unicode},
|
||||
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 when C{root} has been
|
||||
completely flattened into C{write} or which will be errbacked if an
|
||||
unexpected exception occurs.
|
||||
"""
|
||||
result = Deferred()
|
||||
state = _flattenTree(request, root, write)
|
||||
_writeFlattenedData(state, write, result)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def flattenString(request, root):
|
||||
"""
|
||||
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 string as
|
||||
its result when C{root} has been completely flattened into C{write} or
|
||||
which will be errbacked if an unexpected exception occurs.
|
||||
"""
|
||||
io = BytesIO()
|
||||
d = flatten(request, root, io.write)
|
||||
d.addCallback(lambda _: io.getvalue())
|
||||
return d
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
# -*- test-case-name: twisted.web.test.test_html -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""I hold HTML generation helpers.
|
||||
"""
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.python.compat import NativeStringIO as StringIO, escape
|
||||
from twisted.python.deprecate import deprecated
|
||||
from incremental import Version
|
||||
|
||||
|
||||
|
||||
@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('<li> <a href="%s">%s</a></li>\n' % (hr, el))
|
||||
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:
|
||||
log.msg("Error calling %r:" % (func,))
|
||||
log.err()
|
||||
return PRE("An error occurred.")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,828 @@
|
||||
# -*- 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 zope.interface import Interface, Attribute
|
||||
|
||||
from twisted.internet.interfaces import IPushProducer
|
||||
from twisted.cred.credentials import IUsernameDigestHash
|
||||
|
||||
|
||||
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 {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 user passed in to the request.
|
||||
|
||||
This will either use the Host: header (if it is available) or the
|
||||
host we are listening on if the header is unavailable.
|
||||
|
||||
@returns: the requested hostname
|
||||
@rtype: L{str}
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
@type: 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.
|
||||
@type: 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}, L{long} 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 peer: L{twisted.web.http.Request}
|
||||
@param peer: 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):
|
||||
"""
|
||||
Look up and return the render method associated with the given name.
|
||||
|
||||
@type name: L{str}
|
||||
@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):
|
||||
"""
|
||||
Get the document for this L{IRenderable}.
|
||||
|
||||
@type request: L{IRequest} provider or L{None}
|
||||
@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():
|
||||
"""
|
||||
Load a template suitable for rendering.
|
||||
|
||||
@return: a L{list} of L{list}s, L{unicode} objects, C{Element}s and
|
||||
other 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:
|
||||
|
||||
- ResponseDone, which indicates that all bytes from the response
|
||||
have been successfully delivered.
|
||||
|
||||
- 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.
|
||||
|
||||
- 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)
|
||||
redirect = BrowserLikeRedirectAgent(baseAgent, limit=10)
|
||||
authenticate = AuthenticateAgent(
|
||||
redirect, [diskStore.credentials, GtkAuthInterface()])
|
||||
cookie = CookieAgent(authenticate, diskStore.cookie)
|
||||
decode = ContentDecoderAgent(cookie, [(b"gzip", GzipDecoder())])
|
||||
cache = CacheAgent(decode, diskStore.cache)
|
||||
|
||||
doSomeRequests(cache)
|
||||
"""
|
||||
def request(method, uri, headers=None, bodyProducer=None):
|
||||
"""
|
||||
Request the resource at the given location.
|
||||
|
||||
@param method: The request method to use, such as C{"GET"}, C{"HEAD"},
|
||||
C{"PUT"}, C{"POST"}, etc.
|
||||
@type method: L{bytes}
|
||||
|
||||
@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.
|
||||
@type uri: L{bytes}
|
||||
|
||||
@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).
|
||||
@type headers: L{Headers} or L{None}
|
||||
|
||||
@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.
|
||||
@type bodyProducer: L{IBodyProducer} provider
|
||||
|
||||
@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).
|
||||
@rtype: L{Deferred}
|
||||
"""
|
||||
|
||||
|
||||
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 = u"twisted.web.iweb.UNKNOWN_LENGTH"
|
||||
|
||||
__all__ = [
|
||||
"IUsernameDigestHash", "ICredentialFactory", "IRequest",
|
||||
"IBodyProducer", "IRenderable", "IResponse", "_IRequestEncoder",
|
||||
"_IRequestEncoderFactory", "IClientRequest",
|
||||
|
||||
"UNKNOWN_LENGTH"]
|
||||
@@ -0,0 +1,52 @@
|
||||
# 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
|
||||
@@ -0,0 +1,182 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
import os, traceback
|
||||
|
||||
from twisted import copyright
|
||||
from twisted.python.filepath import _coerceToFilesystemEncoding
|
||||
from twisted.python.compat import execfile, networkString, NativeStringIO, _PY3
|
||||
from twisted.web import http, server, static, resource, 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.ErrorPage(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.ErrorPage(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.NoResource()
|
||||
|
||||
def render(self, request):
|
||||
return resource.NoResource().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 IOError as e:
|
||||
if e.errno == 2: #file not found
|
||||
request.setResponseCode(http.NOT_FOUND)
|
||||
request.write(resource.NoResource("File not found.").render(request))
|
||||
except:
|
||||
io = NativeStringIO()
|
||||
traceback.print_exc(file=io)
|
||||
output = util._PRE(io.getvalue())
|
||||
if _PY3:
|
||||
output = output.encode("utf8")
|
||||
request.write(output)
|
||||
request.finish()
|
||||
return server.NOT_DONE_YET
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,316 @@
|
||||
# -*- 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.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import incremental
|
||||
|
||||
from twisted.application import service, strports
|
||||
from twisted.internet import interfaces, reactor
|
||||
from twisted.python import usage, reflect, threadpool, deprecate
|
||||
from twisted.spread import pb
|
||||
from twisted.web import distrib
|
||||
from twisted.web import resource, server, static, script, demo, wsgi
|
||||
from twisted.web import twcgi
|
||||
|
||||
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("No such WSGI application: %r" % (name,))
|
||||
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
|
||||
@@ -0,0 +1,575 @@
|
||||
# -*- 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}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
__all__ = [
|
||||
'TEMPLATE_NAMESPACE', 'VALID_HTML_TAG_NAMES', 'Element', 'TagLoader',
|
||||
'XMLString', 'XMLFile', 'renderer', 'flatten', 'flattenString', 'tags',
|
||||
'Comment', 'CDATA', 'Tag', 'slot', 'CharRef', 'renderElement'
|
||||
]
|
||||
|
||||
import warnings
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from xml.sax import make_parser, handler
|
||||
|
||||
from twisted.python.compat import NativeStringIO, items
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.web._stan import Tag, slot, Comment, CDATA, CharRef
|
||||
from twisted.web.iweb import ITemplateLoader
|
||||
from twisted.logger import Logger
|
||||
|
||||
TEMPLATE_NAMESPACE = 'http://twistedmatrix.com/ns/twisted.web.template/0.1'
|
||||
|
||||
# Go read the definition of NOT_DONE_YET. For lulz. This is totally
|
||||
# equivalent. And this turns out to be necessary, because trying to import
|
||||
# NOT_DONE_YET in this module causes a circular import which we cannot escape
|
||||
# from. From which we cannot escape. Etc. glyph is okay with this solution for
|
||||
# now, and so am I, as long as this comment stays to explain to future
|
||||
# maintainers what it means. ~ C.
|
||||
#
|
||||
# See http://twistedmatrix.com/trac/ticket/5557 for progress on fixing this.
|
||||
NOT_DONE_YET = 1
|
||||
_moduleLog = Logger()
|
||||
|
||||
|
||||
class _NSContext(object):
|
||||
"""
|
||||
A mapping from XML namespaces onto their prefixes in the document.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""
|
||||
Pull out the parent's namespaces, if there's no parent then default to
|
||||
XML.
|
||||
"""
|
||||
self.parent = parent
|
||||
if parent is not None:
|
||||
self.nss = OrderedDict(parent.nss)
|
||||
else:
|
||||
self.nss = {'http://www.w3.org/XML/1998/namespace':'xml'}
|
||||
|
||||
|
||||
def get(self, k, d=None):
|
||||
"""
|
||||
Get a prefix for a namespace.
|
||||
|
||||
@param d: The default prefix value.
|
||||
"""
|
||||
return self.nss.get(k, d)
|
||||
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
"""
|
||||
Proxy through to setting the prefix for the namespace.
|
||||
"""
|
||||
self.nss.__setitem__(k, v)
|
||||
|
||||
|
||||
def __getitem__(self, k):
|
||||
"""
|
||||
Proxy through to getting the prefix for the namespace.
|
||||
"""
|
||||
return self.nss.__getitem__(k)
|
||||
|
||||
|
||||
|
||||
class _ToStan(handler.ContentHandler, handler.EntityResolver):
|
||||
"""
|
||||
A SAX parser which converts an XML document to the Twisted STAN
|
||||
Document Object Model.
|
||||
"""
|
||||
|
||||
def __init__(self, sourceFilename):
|
||||
"""
|
||||
@param sourceFilename: the filename to load the XML out of.
|
||||
"""
|
||||
self.sourceFilename = sourceFilename
|
||||
self.prefixMap = _NSContext()
|
||||
self.inCDATA = False
|
||||
|
||||
|
||||
def setDocumentLocator(self, locator):
|
||||
"""
|
||||
Set the document locator, which knows about line and character numbers.
|
||||
"""
|
||||
self.locator = locator
|
||||
|
||||
|
||||
def startDocument(self):
|
||||
"""
|
||||
Initialise the document.
|
||||
"""
|
||||
self.document = []
|
||||
self.current = self.document
|
||||
self.stack = []
|
||||
self.xmlnsAttrs = []
|
||||
|
||||
|
||||
def endDocument(self):
|
||||
"""
|
||||
Document ended.
|
||||
"""
|
||||
|
||||
|
||||
def processingInstruction(self, target, data):
|
||||
"""
|
||||
Processing instructions are ignored.
|
||||
"""
|
||||
|
||||
|
||||
def startPrefixMapping(self, prefix, uri):
|
||||
"""
|
||||
Set up the prefix mapping, which maps fully qualified namespace URIs
|
||||
onto namespace prefixes.
|
||||
|
||||
This gets called before startElementNS whenever an C{xmlns} attribute
|
||||
is seen.
|
||||
"""
|
||||
|
||||
self.prefixMap = _NSContext(self.prefixMap)
|
||||
self.prefixMap[uri] = prefix
|
||||
|
||||
# Ignore the template namespace; we'll replace those during parsing.
|
||||
if uri == TEMPLATE_NAMESPACE:
|
||||
return
|
||||
|
||||
# Add to a list that will be applied once we have the element.
|
||||
if prefix is None:
|
||||
self.xmlnsAttrs.append(('xmlns',uri))
|
||||
else:
|
||||
self.xmlnsAttrs.append(('xmlns:%s'%prefix,uri))
|
||||
|
||||
|
||||
def endPrefixMapping(self, prefix):
|
||||
"""
|
||||
"Pops the stack" on the prefix mapping.
|
||||
|
||||
Gets called after endElementNS.
|
||||
"""
|
||||
self.prefixMap = self.prefixMap.parent
|
||||
|
||||
|
||||
def startElementNS(self, namespaceAndName, qname, attrs):
|
||||
"""
|
||||
Gets called when we encounter a new xmlns attribute.
|
||||
|
||||
@param namespaceAndName: a (namespace, name) tuple, where name
|
||||
determines which type of action to take, if the namespace matches
|
||||
L{TEMPLATE_NAMESPACE}.
|
||||
@param qname: ignored.
|
||||
@param attrs: attributes on the element being started.
|
||||
"""
|
||||
|
||||
filename = self.sourceFilename
|
||||
lineNumber = self.locator.getLineNumber()
|
||||
columnNumber = self.locator.getColumnNumber()
|
||||
|
||||
ns, name = namespaceAndName
|
||||
if ns == TEMPLATE_NAMESPACE:
|
||||
if name == 'transparent':
|
||||
name = ''
|
||||
elif name == 'slot':
|
||||
try:
|
||||
# Try to get the default value for the slot
|
||||
default = attrs[(None, 'default')]
|
||||
except KeyError:
|
||||
# If there wasn't one, then use None to indicate no
|
||||
# default.
|
||||
default = None
|
||||
el = slot(
|
||||
attrs[(None, 'name')], default=default,
|
||||
filename=filename, lineNumber=lineNumber,
|
||||
columnNumber=columnNumber)
|
||||
self.stack.append(el)
|
||||
self.current.append(el)
|
||||
self.current = el.children
|
||||
return
|
||||
|
||||
render = None
|
||||
|
||||
attrs = OrderedDict(attrs)
|
||||
for k, v in items(attrs):
|
||||
attrNS, justTheName = k
|
||||
if attrNS != TEMPLATE_NAMESPACE:
|
||||
continue
|
||||
if justTheName == 'render':
|
||||
render = v
|
||||
del attrs[k]
|
||||
|
||||
# nonTemplateAttrs is a dictionary mapping attributes that are *not* in
|
||||
# TEMPLATE_NAMESPACE to their values. Those in TEMPLATE_NAMESPACE were
|
||||
# just removed from 'attrs' in the loop immediately above. The key in
|
||||
# nonTemplateAttrs is either simply the attribute name (if it was not
|
||||
# specified as having a namespace in the template) or prefix:name,
|
||||
# preserving the xml namespace prefix given in the document.
|
||||
|
||||
nonTemplateAttrs = OrderedDict()
|
||||
for (attrNs, attrName), v in items(attrs):
|
||||
nsPrefix = self.prefixMap.get(attrNs)
|
||||
if nsPrefix is None:
|
||||
attrKey = attrName
|
||||
else:
|
||||
attrKey = '%s:%s' % (nsPrefix, attrName)
|
||||
nonTemplateAttrs[attrKey] = v
|
||||
|
||||
if ns == TEMPLATE_NAMESPACE and name == 'attr':
|
||||
if not self.stack:
|
||||
# TODO: define a better exception for this?
|
||||
raise AssertionError(
|
||||
'<{%s}attr> as top-level element' % (TEMPLATE_NAMESPACE,))
|
||||
if 'name' not in nonTemplateAttrs:
|
||||
# TODO: same here
|
||||
raise AssertionError(
|
||||
'<{%s}attr> requires a name attribute' % (TEMPLATE_NAMESPACE,))
|
||||
el = Tag('', render=render, filename=filename,
|
||||
lineNumber=lineNumber, columnNumber=columnNumber)
|
||||
self.stack[-1].attributes[nonTemplateAttrs['name']] = el
|
||||
self.stack.append(el)
|
||||
self.current = el.children
|
||||
return
|
||||
|
||||
# Apply any xmlns attributes
|
||||
if self.xmlnsAttrs:
|
||||
nonTemplateAttrs.update(OrderedDict(self.xmlnsAttrs))
|
||||
self.xmlnsAttrs = []
|
||||
|
||||
# Add the prefix that was used in the parsed template for non-template
|
||||
# namespaces (which will not be consumed anyway).
|
||||
if ns != TEMPLATE_NAMESPACE and ns is not None:
|
||||
prefix = self.prefixMap[ns]
|
||||
if prefix is not None:
|
||||
name = '%s:%s' % (self.prefixMap[ns],name)
|
||||
el = Tag(
|
||||
name, attributes=OrderedDict(nonTemplateAttrs), render=render,
|
||||
filename=filename, lineNumber=lineNumber,
|
||||
columnNumber=columnNumber)
|
||||
self.stack.append(el)
|
||||
self.current.append(el)
|
||||
self.current = el.children
|
||||
|
||||
|
||||
def characters(self, ch):
|
||||
"""
|
||||
Called when we receive some characters. CDATA characters get passed
|
||||
through as is.
|
||||
|
||||
@type ch: C{string}
|
||||
"""
|
||||
if self.inCDATA:
|
||||
self.stack[-1].append(ch)
|
||||
return
|
||||
self.current.append(ch)
|
||||
|
||||
|
||||
def endElementNS(self, name, qname):
|
||||
"""
|
||||
A namespace tag is closed. Pop the stack, if there's anything left in
|
||||
it, otherwise return to the document's namespace.
|
||||
"""
|
||||
self.stack.pop()
|
||||
if self.stack:
|
||||
self.current = self.stack[-1].children
|
||||
else:
|
||||
self.current = self.document
|
||||
|
||||
|
||||
def startDTD(self, name, publicId, systemId):
|
||||
"""
|
||||
DTDs are ignored.
|
||||
"""
|
||||
|
||||
|
||||
def endDTD(self, *args):
|
||||
"""
|
||||
DTDs are ignored.
|
||||
"""
|
||||
|
||||
|
||||
def startCDATA(self):
|
||||
"""
|
||||
We're starting to be in a CDATA element, make a note of this.
|
||||
"""
|
||||
self.inCDATA = True
|
||||
self.stack.append([])
|
||||
|
||||
|
||||
def endCDATA(self):
|
||||
"""
|
||||
We're no longer in a CDATA element. Collect up the characters we've
|
||||
parsed and put them in a new CDATA object.
|
||||
"""
|
||||
self.inCDATA = False
|
||||
comment = ''.join(self.stack.pop())
|
||||
self.current.append(CDATA(comment))
|
||||
|
||||
|
||||
def comment(self, content):
|
||||
"""
|
||||
Add an XML comment which we've encountered.
|
||||
"""
|
||||
self.current.append(Comment(content))
|
||||
|
||||
|
||||
|
||||
def _flatsaxParse(fl):
|
||||
"""
|
||||
Perform a SAX parse of an XML document with the _ToStan class.
|
||||
|
||||
@param fl: The XML document to be parsed.
|
||||
@type fl: A file object or filename.
|
||||
|
||||
@return: a C{list} of Stan objects.
|
||||
"""
|
||||
parser = make_parser()
|
||||
parser.setFeature(handler.feature_validation, 0)
|
||||
parser.setFeature(handler.feature_namespaces, 1)
|
||||
parser.setFeature(handler.feature_external_ges, 0)
|
||||
parser.setFeature(handler.feature_external_pes, 0)
|
||||
|
||||
s = _ToStan(getattr(fl, "name", None))
|
||||
parser.setContentHandler(s)
|
||||
parser.setEntityResolver(s)
|
||||
parser.setProperty(handler.property_lexical_handler, s)
|
||||
|
||||
parser.parse(fl)
|
||||
|
||||
return s.document
|
||||
|
||||
|
||||
@implementer(ITemplateLoader)
|
||||
class TagLoader(object):
|
||||
"""
|
||||
An L{ITemplateLoader} that loads existing L{IRenderable} providers.
|
||||
|
||||
@ivar tag: The object which will be loaded.
|
||||
@type tag: An L{IRenderable} provider.
|
||||
"""
|
||||
|
||||
def __init__(self, tag):
|
||||
"""
|
||||
@param tag: The object which will be loaded.
|
||||
@type tag: An L{IRenderable} provider.
|
||||
"""
|
||||
self.tag = tag
|
||||
|
||||
|
||||
def load(self):
|
||||
return [self.tag]
|
||||
|
||||
|
||||
|
||||
@implementer(ITemplateLoader)
|
||||
class XMLString(object):
|
||||
"""
|
||||
An L{ITemplateLoader} that loads and parses XML from a string.
|
||||
|
||||
@ivar _loadedTemplate: The loaded document.
|
||||
@type _loadedTemplate: a C{list} of Stan objects.
|
||||
"""
|
||||
|
||||
def __init__(self, s):
|
||||
"""
|
||||
Run the parser on a L{NativeStringIO} copy of the string.
|
||||
|
||||
@param s: The string from which to load the XML.
|
||||
@type s: C{str}, or a UTF-8 encoded L{bytes}.
|
||||
"""
|
||||
if not isinstance(s, str):
|
||||
s = s.decode('utf8')
|
||||
|
||||
self._loadedTemplate = _flatsaxParse(NativeStringIO(s))
|
||||
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Return the document.
|
||||
|
||||
@return: the loaded document.
|
||||
@rtype: a C{list} of Stan objects.
|
||||
"""
|
||||
return self._loadedTemplate
|
||||
|
||||
|
||||
|
||||
@implementer(ITemplateLoader)
|
||||
class XMLFile(object):
|
||||
"""
|
||||
An L{ITemplateLoader} that loads and parses XML from a file.
|
||||
|
||||
@ivar _loadedTemplate: The loaded document, or L{None}, if not loaded.
|
||||
@type _loadedTemplate: a C{list} of Stan objects, or L{None}.
|
||||
|
||||
@ivar _path: The L{FilePath}, file object, or filename that is being
|
||||
loaded from.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
Run the parser on a file.
|
||||
|
||||
@param path: The file from which to load the XML.
|
||||
@type path: L{FilePath}
|
||||
"""
|
||||
if not isinstance(path, FilePath):
|
||||
warnings.warn(
|
||||
"Passing filenames or file objects to XMLFile is deprecated "
|
||||
"since Twisted 12.1. Pass a FilePath instead.",
|
||||
category=DeprecationWarning, stacklevel=2)
|
||||
self._loadedTemplate = None
|
||||
self._path = path
|
||||
|
||||
|
||||
def _loadDoc(self):
|
||||
"""
|
||||
Read and parse the XML.
|
||||
|
||||
@return: the loaded document.
|
||||
@rtype: a C{list} of Stan objects.
|
||||
"""
|
||||
if not isinstance(self._path, FilePath):
|
||||
return _flatsaxParse(self._path)
|
||||
else:
|
||||
with self._path.open('r') as f:
|
||||
return _flatsaxParse(f)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return '<XMLFile of %r>' % (self._path,)
|
||||
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
Return the document, first loading it if necessary.
|
||||
|
||||
@return: the loaded document.
|
||||
@rtype: a C{list} of Stan objects.
|
||||
"""
|
||||
if self._loadedTemplate is None:
|
||||
self._loadedTemplate = self._loadDoc()
|
||||
return self._loadedTemplate
|
||||
|
||||
|
||||
|
||||
# Last updated October 2011, using W3Schools as a reference. Link:
|
||||
# http://www.w3schools.com/html5/html5_reference.asp
|
||||
# Note that <xmp> is explicitly omitted; its semantics do not work with
|
||||
# t.w.template and it is officially deprecated.
|
||||
VALID_HTML_TAG_NAMES = set([
|
||||
'a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside',
|
||||
'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'big', 'blockquote',
|
||||
'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code',
|
||||
'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 'dfn',
|
||||
'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption',
|
||||
'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3',
|
||||
'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe',
|
||||
'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'legend',
|
||||
'li', 'link', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes',
|
||||
'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param',
|
||||
'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script',
|
||||
'section', 'select', 'small', 'source', 'span', 'strike', 'strong',
|
||||
'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea',
|
||||
'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'tt', 'u', 'ul', 'var',
|
||||
'video', 'wbr',
|
||||
])
|
||||
|
||||
|
||||
|
||||
class _TagFactory(object):
|
||||
"""
|
||||
A factory for L{Tag} objects; the implementation of the L{tags} object.
|
||||
|
||||
This allows for the syntactic convenience of C{from twisted.web.html import
|
||||
tags; tags.a(href="linked-page.html")}, where 'a' can be basically any HTML
|
||||
tag.
|
||||
|
||||
The class is not exposed publicly because you only ever need one of these,
|
||||
and we already made it for you.
|
||||
|
||||
@see: L{tags}
|
||||
"""
|
||||
def __getattr__(self, tagName):
|
||||
if tagName == 'transparent':
|
||||
return Tag('')
|
||||
# allow for E.del as E.del_
|
||||
tagName = tagName.rstrip('_')
|
||||
if tagName not in VALID_HTML_TAG_NAMES:
|
||||
raise AttributeError('unknown tag %r' % (tagName,))
|
||||
return Tag(tagName)
|
||||
|
||||
|
||||
|
||||
tags = _TagFactory()
|
||||
|
||||
|
||||
|
||||
def renderElement(request, element,
|
||||
doctype=b'<!DOCTYPE html>', _failElement=None):
|
||||
"""
|
||||
Render an element or other C{IRenderable}.
|
||||
|
||||
@param request: The C{Request} being rendered to.
|
||||
@param element: An C{IRenderable} which will be rendered.
|
||||
@param doctype: A C{bytes} which will be written as the first line of
|
||||
the request, or L{None} to disable writing of a doctype. The C{string}
|
||||
should not include a trailing newline and will default to the HTML5
|
||||
doctype C{'<!DOCTYPE html>'}.
|
||||
|
||||
@returns: NOT_DONE_YET
|
||||
|
||||
@since: 12.1
|
||||
"""
|
||||
if doctype is not None:
|
||||
request.write(doctype)
|
||||
request.write(b'\n')
|
||||
|
||||
if _failElement is None:
|
||||
_failElement = twisted.web.util.FailureElement
|
||||
|
||||
d = flatten(request, element, request.write)
|
||||
|
||||
def eb(failure):
|
||||
_moduleLog.failure(
|
||||
"An error occurred while rendering the response.",
|
||||
failure=failure
|
||||
)
|
||||
if request.site.displayTracebacks:
|
||||
return flatten(request, _failElement(failure),
|
||||
request.write).encode('utf8')
|
||||
else:
|
||||
request.write(
|
||||
(b'<div style="font-size:800%;'
|
||||
b'background-color:#FFF;'
|
||||
b'color:#F00'
|
||||
b'">An error occurred while rendering the response.</div>'))
|
||||
|
||||
d.addErrback(eb)
|
||||
d.addBoth(lambda _: request.finish())
|
||||
return NOT_DONE_YET
|
||||
|
||||
|
||||
|
||||
from twisted.web._element import Element, renderer
|
||||
from twisted.web._flatten import flatten, flattenString
|
||||
import twisted.web.util
|
||||
@@ -0,0 +1,103 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
General helpers for L{twisted.web} unit tests.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.internet.defer import succeed
|
||||
from twisted.web import server
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.failure import Failure
|
||||
|
||||
from twisted.web._flatten import flattenString
|
||||
from twisted.web.error import FlattenerError
|
||||
|
||||
|
||||
|
||||
def _render(resource, request):
|
||||
result = resource.render(request)
|
||||
if isinstance(result, bytes):
|
||||
request.write(result)
|
||||
request.finish()
|
||||
return succeed(None)
|
||||
elif result is server.NOT_DONE_YET:
|
||||
if request.finished:
|
||||
return succeed(None)
|
||||
else:
|
||||
return request.notifyFinish()
|
||||
else:
|
||||
raise ValueError("Unexpected return value: %r" % (result,))
|
||||
|
||||
|
||||
|
||||
class FlattenTestCase(TestCase):
|
||||
"""
|
||||
A test case that assists with testing L{twisted.web._flatten}.
|
||||
"""
|
||||
def assertFlattensTo(self, root, target):
|
||||
"""
|
||||
Assert that a root element, when flattened, is equal to a string.
|
||||
"""
|
||||
d = flattenString(None, root)
|
||||
d.addCallback(lambda s: self.assertEqual(s, target))
|
||||
return d
|
||||
|
||||
|
||||
def assertFlattensImmediately(self, root, target):
|
||||
"""
|
||||
Assert that a root element, when flattened, is equal to a string, and
|
||||
performs no asynchronus Deferred anything.
|
||||
|
||||
This version is more convenient in tests which wish to make multiple
|
||||
assertions about flattening, since it can be called multiple times
|
||||
without having to add multiple callbacks.
|
||||
|
||||
@return: the result of rendering L{root}, which should be equivalent to
|
||||
L{target}.
|
||||
@rtype: L{bytes}
|
||||
"""
|
||||
results = []
|
||||
it = self.assertFlattensTo(root, target)
|
||||
it.addBoth(results.append)
|
||||
# Do our best to clean it up if something goes wrong.
|
||||
self.addCleanup(it.cancel)
|
||||
if not results:
|
||||
self.fail("Rendering did not complete immediately.")
|
||||
result = results[0]
|
||||
if isinstance(result, Failure):
|
||||
result.raiseException()
|
||||
return results[0]
|
||||
|
||||
|
||||
def assertFlatteningRaises(self, root, exn):
|
||||
"""
|
||||
Assert flattening a root element raises a particular exception.
|
||||
"""
|
||||
d = self.assertFailure(self.assertFlattensTo(root, b''), FlattenerError)
|
||||
d.addCallback(lambda exc: self.assertIsInstance(exc._exception, exn))
|
||||
return d
|
||||
|
||||
|
||||
|
||||
def assertIsFilesystemTemporary(case, fileObj):
|
||||
"""
|
||||
Assert that C{fileObj} is a temporary file on the filesystem.
|
||||
|
||||
@param case: A C{TestCase} instance to use to make the assertion.
|
||||
|
||||
@raise: C{case.failureException} if C{fileObj} is not a temporary file on
|
||||
the filesystem.
|
||||
"""
|
||||
# The tempfile API used to create content returns an instance of a
|
||||
# different type depending on what platform we're running on. The point
|
||||
# here is to verify that the request body is in a file that's on the
|
||||
# filesystem. Having a fileno method that returns an int is a somewhat
|
||||
# close approximation of this. -exarkun
|
||||
case.assertIsInstance(fileObj.fileno(), int)
|
||||
|
||||
|
||||
|
||||
__all__ = ["_render", "FlattenTestCase", "assertIsFilesystemTemporary"]
|
||||
@@ -0,0 +1,527 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.web.distrib}.
|
||||
"""
|
||||
|
||||
from os.path import abspath
|
||||
from xml.dom.minidom import parseString
|
||||
try:
|
||||
import pwd
|
||||
except ImportError:
|
||||
pwd = None
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.python import filepath, failure
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.trial import unittest
|
||||
from twisted.spread import pb
|
||||
from twisted.spread.banana import SIZE_LIMIT
|
||||
from twisted.web import distrib, client, resource, static, server
|
||||
from twisted.web.test.test_web import DummyRequest, DummyChannel
|
||||
from twisted.web.test._util import _render
|
||||
from twisted.test import proto_helpers
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.logger import globalLogPublisher
|
||||
|
||||
|
||||
class MySite(server.Site):
|
||||
pass
|
||||
|
||||
|
||||
class PBServerFactory(pb.PBServerFactory):
|
||||
"""
|
||||
A PB server factory which keeps track of the most recent protocol it
|
||||
created.
|
||||
|
||||
@ivar proto: L{None} or the L{Broker} instance most recently returned
|
||||
from C{buildProtocol}.
|
||||
"""
|
||||
proto = None
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
self.proto = pb.PBServerFactory.buildProtocol(self, addr)
|
||||
return self.proto
|
||||
|
||||
|
||||
|
||||
class ArbitraryError(Exception):
|
||||
"""
|
||||
An exception for this test.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class DistribTests(unittest.TestCase):
|
||||
port1 = None
|
||||
port2 = None
|
||||
sub = None
|
||||
f1 = None
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Clean up all the event sources left behind by either directly by
|
||||
test methods or indirectly via some distrib API.
|
||||
"""
|
||||
dl = [defer.Deferred(), defer.Deferred()]
|
||||
if self.f1 is not None and self.f1.proto is not None:
|
||||
self.f1.proto.notifyOnDisconnect(lambda: dl[0].callback(None))
|
||||
else:
|
||||
dl[0].callback(None)
|
||||
if self.sub is not None and self.sub.publisher is not None:
|
||||
self.sub.publisher.broker.notifyOnDisconnect(
|
||||
lambda: dl[1].callback(None))
|
||||
self.sub.publisher.broker.transport.loseConnection()
|
||||
else:
|
||||
dl[1].callback(None)
|
||||
if self.port1 is not None:
|
||||
dl.append(self.port1.stopListening())
|
||||
if self.port2 is not None:
|
||||
dl.append(self.port2.stopListening())
|
||||
return defer.gatherResults(dl)
|
||||
|
||||
|
||||
def testDistrib(self):
|
||||
# site1 is the publisher
|
||||
r1 = resource.Resource()
|
||||
r1.putChild(b"there", static.Data(b"root", "text/plain"))
|
||||
site1 = server.Site(r1)
|
||||
self.f1 = PBServerFactory(distrib.ResourcePublisher(site1))
|
||||
self.port1 = reactor.listenTCP(0, self.f1)
|
||||
self.sub = distrib.ResourceSubscription("127.0.0.1",
|
||||
self.port1.getHost().port)
|
||||
r2 = resource.Resource()
|
||||
r2.putChild(b"here", self.sub)
|
||||
f2 = MySite(r2)
|
||||
self.port2 = reactor.listenTCP(0, f2)
|
||||
agent = client.Agent(reactor)
|
||||
url = "http://127.0.0.1:{}/here/there".format(
|
||||
self.port2.getHost().port)
|
||||
url = url.encode("ascii")
|
||||
d = agent.request(b"GET", url)
|
||||
d.addCallback(client.readBody)
|
||||
d.addCallback(self.assertEqual, b'root')
|
||||
return d
|
||||
|
||||
|
||||
def _setupDistribServer(self, child):
|
||||
"""
|
||||
Set up a resource on a distrib site using L{ResourcePublisher}.
|
||||
|
||||
@param child: The resource to publish using distrib.
|
||||
|
||||
@return: A tuple consisting of the host and port on which to contact
|
||||
the created site.
|
||||
"""
|
||||
distribRoot = resource.Resource()
|
||||
distribRoot.putChild(b"child", child)
|
||||
distribSite = server.Site(distribRoot)
|
||||
self.f1 = distribFactory = PBServerFactory(
|
||||
distrib.ResourcePublisher(distribSite))
|
||||
distribPort = reactor.listenTCP(
|
||||
0, distribFactory, interface="127.0.0.1")
|
||||
self.addCleanup(distribPort.stopListening)
|
||||
addr = distribPort.getHost()
|
||||
|
||||
self.sub = mainRoot = distrib.ResourceSubscription(
|
||||
addr.host, addr.port)
|
||||
mainSite = server.Site(mainRoot)
|
||||
mainPort = reactor.listenTCP(0, mainSite, interface="127.0.0.1")
|
||||
self.addCleanup(mainPort.stopListening)
|
||||
mainAddr = mainPort.getHost()
|
||||
|
||||
return mainPort, mainAddr
|
||||
|
||||
|
||||
def _requestTest(self, child, **kwargs):
|
||||
"""
|
||||
Set up a resource on a distrib site using L{ResourcePublisher} and
|
||||
then retrieve it from a L{ResourceSubscription} via an HTTP client.
|
||||
|
||||
@param child: The resource to publish using distrib.
|
||||
@param **kwargs: Extra keyword arguments to pass to L{Agent.request} when
|
||||
requesting the resource.
|
||||
|
||||
@return: A L{Deferred} which fires with the result of the request.
|
||||
"""
|
||||
mainPort, mainAddr = self._setupDistribServer(child)
|
||||
agent = client.Agent(reactor)
|
||||
url = "http://%s:%s/child" % (mainAddr.host, mainAddr.port)
|
||||
url = url.encode("ascii")
|
||||
d = agent.request(b"GET", url, **kwargs)
|
||||
d.addCallback(client.readBody)
|
||||
return d
|
||||
|
||||
|
||||
def _requestAgentTest(self, child, **kwargs):
|
||||
"""
|
||||
Set up a resource on a distrib site using L{ResourcePublisher} and
|
||||
then retrieve it from a L{ResourceSubscription} via an HTTP client.
|
||||
|
||||
@param child: The resource to publish using distrib.
|
||||
@param **kwargs: Extra keyword arguments to pass to L{Agent.request} when
|
||||
requesting the resource.
|
||||
|
||||
@return: A L{Deferred} which fires with a tuple consisting of a
|
||||
L{twisted.test.proto_helpers.AccumulatingProtocol} containing the
|
||||
body of the response and an L{IResponse} with the response itself.
|
||||
"""
|
||||
mainPort, mainAddr = self._setupDistribServer(child)
|
||||
|
||||
url = "http://{}:{}/child".format(mainAddr.host, mainAddr.port)
|
||||
url = url.encode("ascii")
|
||||
d = client.Agent(reactor).request(b"GET", url, **kwargs)
|
||||
|
||||
def cbCollectBody(response):
|
||||
protocol = proto_helpers.AccumulatingProtocol()
|
||||
response.deliverBody(protocol)
|
||||
d = protocol.closedDeferred = defer.Deferred()
|
||||
d.addCallback(lambda _: (protocol, response))
|
||||
return d
|
||||
d.addCallback(cbCollectBody)
|
||||
return d
|
||||
|
||||
|
||||
def test_requestHeaders(self):
|
||||
"""
|
||||
The request headers are available on the request object passed to a
|
||||
distributed resource's C{render} method.
|
||||
"""
|
||||
requestHeaders = {}
|
||||
logObserver = proto_helpers.EventLoggingObserver()
|
||||
globalLogPublisher.addObserver(logObserver)
|
||||
req = [None]
|
||||
|
||||
|
||||
class ReportRequestHeaders(resource.Resource):
|
||||
def render(self, request):
|
||||
req[0] = request
|
||||
requestHeaders.update(dict(
|
||||
request.requestHeaders.getAllRawHeaders()))
|
||||
return b""
|
||||
|
||||
def check_logs():
|
||||
msgs = [e["log_format"] for e in logObserver]
|
||||
self.assertIn('connected to publisher', msgs)
|
||||
self.assertIn(
|
||||
"could not connect to distributed web service: {msg}",
|
||||
msgs
|
||||
)
|
||||
self.assertIn(req[0], msgs)
|
||||
globalLogPublisher.removeObserver(logObserver)
|
||||
|
||||
request = self._requestTest(
|
||||
ReportRequestHeaders(), headers=Headers({'foo': ['bar']}))
|
||||
def cbRequested(result):
|
||||
self.f1.proto.notifyOnDisconnect(check_logs)
|
||||
self.assertEqual(requestHeaders[b'Foo'], [b'bar'])
|
||||
|
||||
request.addCallback(cbRequested)
|
||||
return request
|
||||
|
||||
|
||||
def test_requestResponseCode(self):
|
||||
"""
|
||||
The response code can be set by the request object passed to a
|
||||
distributed resource's C{render} method.
|
||||
"""
|
||||
class SetResponseCode(resource.Resource):
|
||||
def render(self, request):
|
||||
request.setResponseCode(200)
|
||||
return ""
|
||||
|
||||
request = self._requestAgentTest(SetResponseCode())
|
||||
def cbRequested(result):
|
||||
self.assertEqual(result[0].data, b"")
|
||||
self.assertEqual(result[1].code, 200)
|
||||
self.assertEqual(result[1].phrase, b"OK")
|
||||
request.addCallback(cbRequested)
|
||||
return request
|
||||
|
||||
|
||||
def test_requestResponseCodeMessage(self):
|
||||
"""
|
||||
The response code and message can be set by the request object passed to
|
||||
a distributed resource's C{render} method.
|
||||
"""
|
||||
class SetResponseCode(resource.Resource):
|
||||
def render(self, request):
|
||||
request.setResponseCode(200, b"some-message")
|
||||
return ""
|
||||
|
||||
request = self._requestAgentTest(SetResponseCode())
|
||||
def cbRequested(result):
|
||||
self.assertEqual(result[0].data, b"")
|
||||
self.assertEqual(result[1].code, 200)
|
||||
self.assertEqual(result[1].phrase, b"some-message")
|
||||
request.addCallback(cbRequested)
|
||||
return request
|
||||
|
||||
|
||||
def test_largeWrite(self):
|
||||
"""
|
||||
If a string longer than the Banana size limit is passed to the
|
||||
L{distrib.Request} passed to the remote resource, it is broken into
|
||||
smaller strings to be transported over the PB connection.
|
||||
"""
|
||||
class LargeWrite(resource.Resource):
|
||||
def render(self, request):
|
||||
request.write(b'x' * SIZE_LIMIT + b'y')
|
||||
request.finish()
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
request = self._requestTest(LargeWrite())
|
||||
request.addCallback(self.assertEqual, b'x' * SIZE_LIMIT + b'y')
|
||||
return request
|
||||
|
||||
|
||||
def test_largeReturn(self):
|
||||
"""
|
||||
Like L{test_largeWrite}, but for the case where C{render} returns a
|
||||
long string rather than explicitly passing it to L{Request.write}.
|
||||
"""
|
||||
class LargeReturn(resource.Resource):
|
||||
def render(self, request):
|
||||
return b'x' * SIZE_LIMIT + b'y'
|
||||
|
||||
request = self._requestTest(LargeReturn())
|
||||
request.addCallback(self.assertEqual, b'x' * SIZE_LIMIT + b'y')
|
||||
return request
|
||||
|
||||
|
||||
def test_connectionLost(self):
|
||||
"""
|
||||
If there is an error issuing the request to the remote publisher, an
|
||||
error response is returned.
|
||||
"""
|
||||
# Using pb.Root as a publisher will cause request calls to fail with an
|
||||
# error every time. Just what we want to test.
|
||||
self.f1 = serverFactory = PBServerFactory(pb.Root())
|
||||
self.port1 = serverPort = reactor.listenTCP(0, serverFactory)
|
||||
|
||||
self.sub = subscription = distrib.ResourceSubscription(
|
||||
"127.0.0.1", serverPort.getHost().port)
|
||||
request = DummyRequest([b''])
|
||||
d = _render(subscription, request)
|
||||
def cbRendered(ignored):
|
||||
self.assertEqual(request.responseCode, 500)
|
||||
# This is the error we caused the request to fail with. It should
|
||||
# have been logged.
|
||||
errors = self.flushLoggedErrors(pb.NoSuchMethod)
|
||||
self.assertEqual(len(errors), 1)
|
||||
# The error page is rendered as HTML.
|
||||
expected = [
|
||||
b'',
|
||||
b'<html>',
|
||||
b' <head><title>500 - Server Connection Lost</title></head>',
|
||||
b' <body>',
|
||||
b' <h1>Server Connection Lost</h1>',
|
||||
b' <p>Connection to distributed server lost:'
|
||||
b'<pre>'
|
||||
b'[Failure instance: Traceback from remote host -- '
|
||||
b'twisted.spread.flavors.NoSuchMethod: '
|
||||
b'No such method: remote_request',
|
||||
b']</pre></p>',
|
||||
b' </body>',
|
||||
b'</html>',
|
||||
b''
|
||||
]
|
||||
self.assertEqual([b'\n'.join(expected)], request.written)
|
||||
|
||||
d.addCallback(cbRendered)
|
||||
return d
|
||||
|
||||
|
||||
def test_logFailed(self):
|
||||
"""
|
||||
When a request fails, the string form of the failure is logged.
|
||||
"""
|
||||
logObserver = proto_helpers.EventLoggingObserver.createWithCleanup(
|
||||
self,
|
||||
globalLogPublisher
|
||||
)
|
||||
|
||||
f = failure.Failure(ArbitraryError())
|
||||
request = DummyRequest([b''])
|
||||
issue = distrib.Issue(request)
|
||||
issue.failed(f)
|
||||
self.assertEquals(1, len(logObserver))
|
||||
self.assertIn(
|
||||
"Failure instance",
|
||||
logObserver[0]["log_format"]
|
||||
)
|
||||
|
||||
|
||||
def test_requestFail(self):
|
||||
"""
|
||||
When L{twisted.web.distrib.Request}'s fail is called, the failure
|
||||
is logged.
|
||||
"""
|
||||
logObserver = proto_helpers.EventLoggingObserver.createWithCleanup(
|
||||
self,
|
||||
globalLogPublisher
|
||||
)
|
||||
err = ArbitraryError()
|
||||
f = failure.Failure(err)
|
||||
req = distrib.Request(DummyChannel())
|
||||
req.fail(f)
|
||||
self.flushLoggedErrors(ArbitraryError)
|
||||
self.assertEquals(1, len(logObserver))
|
||||
self.assertIs(logObserver[0]["log_failure"], f)
|
||||
|
||||
|
||||
|
||||
class _PasswordDatabase:
|
||||
def __init__(self, users):
|
||||
self._users = users
|
||||
|
||||
|
||||
def getpwall(self):
|
||||
return iter(self._users)
|
||||
|
||||
|
||||
def getpwnam(self, username):
|
||||
for user in self._users:
|
||||
if user[0] == username:
|
||||
return user
|
||||
raise KeyError()
|
||||
|
||||
|
||||
|
||||
class UserDirectoryTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{UserDirectory}, a resource for listing all user resources
|
||||
available on a system.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.alice = ('alice', 'x', 123, 456, 'Alice,,,', self.mktemp(), '/bin/sh')
|
||||
self.bob = ('bob', 'x', 234, 567, 'Bob,,,', self.mktemp(), '/bin/sh')
|
||||
self.database = _PasswordDatabase([self.alice, self.bob])
|
||||
self.directory = distrib.UserDirectory(self.database)
|
||||
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
L{UserDirectory} instances provide L{resource.IResource}.
|
||||
"""
|
||||
self.assertTrue(verifyObject(resource.IResource, self.directory))
|
||||
|
||||
|
||||
def _404Test(self, name):
|
||||
"""
|
||||
Verify that requesting the C{name} child of C{self.directory} results
|
||||
in a 404 response.
|
||||
"""
|
||||
request = DummyRequest([name])
|
||||
result = self.directory.getChild(name, request)
|
||||
d = _render(result, request)
|
||||
def cbRendered(ignored):
|
||||
self.assertEqual(request.responseCode, 404)
|
||||
d.addCallback(cbRendered)
|
||||
return d
|
||||
|
||||
|
||||
def test_getInvalidUser(self):
|
||||
"""
|
||||
L{UserDirectory.getChild} returns a resource which renders a 404
|
||||
response when passed a string which does not correspond to any known
|
||||
user.
|
||||
"""
|
||||
return self._404Test('carol')
|
||||
|
||||
|
||||
def test_getUserWithoutResource(self):
|
||||
"""
|
||||
L{UserDirectory.getChild} returns a resource which renders a 404
|
||||
response when passed a string which corresponds to a known user who has
|
||||
neither a user directory nor a user distrib socket.
|
||||
"""
|
||||
return self._404Test('alice')
|
||||
|
||||
|
||||
def test_getPublicHTMLChild(self):
|
||||
"""
|
||||
L{UserDirectory.getChild} returns a L{static.File} instance when passed
|
||||
the name of a user with a home directory containing a I{public_html}
|
||||
directory.
|
||||
"""
|
||||
home = filepath.FilePath(self.bob[-2])
|
||||
public_html = home.child('public_html')
|
||||
public_html.makedirs()
|
||||
request = DummyRequest(['bob'])
|
||||
result = self.directory.getChild('bob', request)
|
||||
self.assertIsInstance(result, static.File)
|
||||
self.assertEqual(result.path, public_html.path)
|
||||
|
||||
|
||||
def test_getDistribChild(self):
|
||||
"""
|
||||
L{UserDirectory.getChild} returns a L{ResourceSubscription} instance
|
||||
when passed the name of a user suffixed with C{".twistd"} who has a
|
||||
home directory containing a I{.twistd-web-pb} socket.
|
||||
"""
|
||||
home = filepath.FilePath(self.bob[-2])
|
||||
home.makedirs()
|
||||
web = home.child('.twistd-web-pb')
|
||||
request = DummyRequest(['bob'])
|
||||
result = self.directory.getChild('bob.twistd', request)
|
||||
self.assertIsInstance(result, distrib.ResourceSubscription)
|
||||
self.assertEqual(result.host, 'unix')
|
||||
self.assertEqual(abspath(result.port), web.path)
|
||||
|
||||
|
||||
def test_invalidMethod(self):
|
||||
"""
|
||||
L{UserDirectory.render} raises L{UnsupportedMethod} in response to a
|
||||
non-I{GET} request.
|
||||
"""
|
||||
request = DummyRequest([''])
|
||||
request.method = 'POST'
|
||||
self.assertRaises(
|
||||
server.UnsupportedMethod, self.directory.render, request)
|
||||
|
||||
|
||||
def test_render(self):
|
||||
"""
|
||||
L{UserDirectory} renders a list of links to available user content
|
||||
in response to a I{GET} request.
|
||||
"""
|
||||
public_html = filepath.FilePath(self.alice[-2]).child('public_html')
|
||||
public_html.makedirs()
|
||||
web = filepath.FilePath(self.bob[-2])
|
||||
web.makedirs()
|
||||
# This really only works if it's a unix socket, but the implementation
|
||||
# doesn't currently check for that. It probably should someday, and
|
||||
# then skip users with non-sockets.
|
||||
web.child('.twistd-web-pb').setContent(b"")
|
||||
|
||||
request = DummyRequest([''])
|
||||
result = _render(self.directory, request)
|
||||
def cbRendered(ignored):
|
||||
document = parseString(b''.join(request.written))
|
||||
|
||||
# Each user should have an li with a link to their page.
|
||||
[alice, bob] = document.getElementsByTagName('li')
|
||||
self.assertEqual(alice.firstChild.tagName, 'a')
|
||||
self.assertEqual(alice.firstChild.getAttribute('href'), 'alice/')
|
||||
self.assertEqual(alice.firstChild.firstChild.data, 'Alice (file)')
|
||||
self.assertEqual(bob.firstChild.tagName, 'a')
|
||||
self.assertEqual(bob.firstChild.getAttribute('href'), 'bob.twistd/')
|
||||
self.assertEqual(bob.firstChild.firstChild.data, 'Bob (twistd)')
|
||||
|
||||
result.addCallback(cbRendered)
|
||||
return result
|
||||
|
||||
|
||||
def test_passwordDatabase(self):
|
||||
"""
|
||||
If L{UserDirectory} is instantiated with no arguments, it uses the
|
||||
L{pwd} module as its password database.
|
||||
"""
|
||||
directory = distrib.UserDirectory()
|
||||
self.assertIdentical(directory._pwd, pwd)
|
||||
if pwd is None:
|
||||
test_passwordDatabase.skip = "pwd module required"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.web import html
|
||||
|
||||
|
||||
|
||||
class WebHtmlTests(unittest.TestCase):
|
||||
"""
|
||||
Unit tests for L{twisted.web.html}.
|
||||
"""
|
||||
|
||||
def test_deprecation(self):
|
||||
"""
|
||||
Calls to L{twisted.web.html} members emit a deprecation warning.
|
||||
"""
|
||||
def assertDeprecationWarningOf(method):
|
||||
"""
|
||||
Check that a deprecation warning is present.
|
||||
"""
|
||||
warningsShown = self.flushWarnings([self.test_deprecation])
|
||||
self.assertEqual(len(warningsShown), 1)
|
||||
self.assertIdentical(
|
||||
warningsShown[0]['category'], DeprecationWarning)
|
||||
self.assertEqual(
|
||||
warningsShown[0]['message'],
|
||||
'twisted.web.html.%s was deprecated in Twisted 15.3.0; '
|
||||
'please use twisted.web.template instead' % (
|
||||
method,),
|
||||
)
|
||||
|
||||
html.PRE('')
|
||||
assertDeprecationWarningOf('PRE')
|
||||
|
||||
html.UL([])
|
||||
assertDeprecationWarningOf('UL')
|
||||
|
||||
html.linkList([])
|
||||
assertDeprecationWarningOf('linkList')
|
||||
|
||||
html.output(lambda: None)
|
||||
assertDeprecationWarningOf('output')
|
||||
@@ -0,0 +1,660 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.web.http_headers}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.compat import _PY3, unicode
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.web.test.requesthelper import (
|
||||
bytesLinearWhitespaceComponents,
|
||||
sanitizedBytes,
|
||||
textLinearWhitespaceComponents,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def assertSanitized(testCase, components, expected):
|
||||
"""
|
||||
Assert that the components are sanitized to the expected value as
|
||||
both a header name and value, across all of L{Header}'s setters
|
||||
and getters.
|
||||
|
||||
@param testCase: A test case.
|
||||
|
||||
@param components: A sequence of values that contain linear
|
||||
whitespace to use as header names and values; see
|
||||
C{textLinearWhitespaceComponents} and
|
||||
C{bytesLinearWhitespaceComponents}
|
||||
|
||||
@param expected: The expected sanitized form of the component for
|
||||
both headers names and their values.
|
||||
"""
|
||||
for component in components:
|
||||
headers = []
|
||||
headers.append(Headers({component: [component]}))
|
||||
|
||||
added = Headers()
|
||||
added.addRawHeader(component, component)
|
||||
headers.append(added)
|
||||
|
||||
setHeader = Headers()
|
||||
setHeader.setRawHeaders(component, [component])
|
||||
headers.append(setHeader)
|
||||
|
||||
for header in headers:
|
||||
testCase.assertEqual(list(header.getAllRawHeaders()),
|
||||
[(expected, [expected])])
|
||||
testCase.assertEqual(header.getRawHeaders(expected), [expected])
|
||||
|
||||
|
||||
|
||||
class BytesHeadersTests(TestCase):
|
||||
"""
|
||||
Tests for L{Headers}, using L{bytes} arguments for methods.
|
||||
"""
|
||||
def test_sanitizeLinearWhitespace(self):
|
||||
"""
|
||||
Linear whitespace in header names or values is replaced with a
|
||||
single space.
|
||||
"""
|
||||
assertSanitized(self, bytesLinearWhitespaceComponents, sanitizedBytes)
|
||||
|
||||
|
||||
def test_initializer(self):
|
||||
"""
|
||||
The header values passed to L{Headers.__init__} can be retrieved via
|
||||
L{Headers.getRawHeaders}.
|
||||
"""
|
||||
h = Headers({b'Foo': [b'bar']})
|
||||
self.assertEqual(h.getRawHeaders(b'foo'), [b'bar'])
|
||||
|
||||
|
||||
def test_setRawHeaders(self):
|
||||
"""
|
||||
L{Headers.setRawHeaders} sets the header values for the given
|
||||
header name to the sequence of byte string values.
|
||||
"""
|
||||
rawValue = [b"value1", b"value2"]
|
||||
h = Headers()
|
||||
h.setRawHeaders(b"test", rawValue)
|
||||
self.assertTrue(h.hasHeader(b"test"))
|
||||
self.assertTrue(h.hasHeader(b"Test"))
|
||||
self.assertEqual(h.getRawHeaders(b"test"), rawValue)
|
||||
|
||||
|
||||
def test_rawHeadersTypeChecking(self):
|
||||
"""
|
||||
L{Headers.setRawHeaders} requires values to be of type list.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertRaises(TypeError, h.setRawHeaders, b'key', {b'Foo': b'bar'})
|
||||
|
||||
|
||||
def test_addRawHeader(self):
|
||||
"""
|
||||
L{Headers.addRawHeader} adds a new value for a given header.
|
||||
"""
|
||||
h = Headers()
|
||||
h.addRawHeader(b"test", b"lemur")
|
||||
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur"])
|
||||
h.addRawHeader(b"test", b"panda")
|
||||
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur", b"panda"])
|
||||
|
||||
|
||||
def test_getRawHeadersNoDefault(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns L{None} if the header is not found and
|
||||
no default is specified.
|
||||
"""
|
||||
self.assertIsNone(Headers().getRawHeaders(b"test"))
|
||||
|
||||
|
||||
def test_getRawHeadersDefaultValue(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns the specified default value when no
|
||||
header is found.
|
||||
"""
|
||||
h = Headers()
|
||||
default = object()
|
||||
self.assertIdentical(h.getRawHeaders(b"test", default), default)
|
||||
|
||||
|
||||
def test_getRawHeadersWithDefaultMatchingValue(self):
|
||||
"""
|
||||
If the object passed as the value list to L{Headers.setRawHeaders}
|
||||
is later passed as a default to L{Headers.getRawHeaders}, the
|
||||
result nevertheless contains encoded values.
|
||||
"""
|
||||
h = Headers()
|
||||
default = [u"value"]
|
||||
h.setRawHeaders(b"key", default)
|
||||
self.assertIsInstance(h.getRawHeaders(b"key", default)[0], bytes)
|
||||
self.assertEqual(h.getRawHeaders(b"key", default), [b"value"])
|
||||
|
||||
|
||||
def test_getRawHeaders(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns the values which have been set for a
|
||||
given header.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(b"test", [b"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(b"Test"), [b"lemur"])
|
||||
|
||||
|
||||
def test_hasHeaderTrue(self):
|
||||
"""
|
||||
Check that L{Headers.hasHeader} returns C{True} when the given header
|
||||
is found.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(b"test", [b"lemur"])
|
||||
self.assertTrue(h.hasHeader(b"test"))
|
||||
self.assertTrue(h.hasHeader(b"Test"))
|
||||
|
||||
|
||||
def test_hasHeaderFalse(self):
|
||||
"""
|
||||
L{Headers.hasHeader} returns C{False} when the given header is not
|
||||
found.
|
||||
"""
|
||||
self.assertFalse(Headers().hasHeader(b"test"))
|
||||
|
||||
|
||||
def test_removeHeader(self):
|
||||
"""
|
||||
Check that L{Headers.removeHeader} removes the given header.
|
||||
"""
|
||||
h = Headers()
|
||||
|
||||
h.setRawHeaders(b"foo", [b"lemur"])
|
||||
self.assertTrue(h.hasHeader(b"foo"))
|
||||
h.removeHeader(b"foo")
|
||||
self.assertFalse(h.hasHeader(b"foo"))
|
||||
|
||||
h.setRawHeaders(b"bar", [b"panda"])
|
||||
self.assertTrue(h.hasHeader(b"bar"))
|
||||
h.removeHeader(b"Bar")
|
||||
self.assertFalse(h.hasHeader(b"bar"))
|
||||
|
||||
|
||||
def test_removeHeaderDoesntExist(self):
|
||||
"""
|
||||
L{Headers.removeHeader} is a no-operation when the specified header is
|
||||
not found.
|
||||
"""
|
||||
h = Headers()
|
||||
h.removeHeader(b"test")
|
||||
self.assertEqual(list(h.getAllRawHeaders()), [])
|
||||
|
||||
|
||||
def test_canonicalNameCaps(self):
|
||||
"""
|
||||
L{Headers._canonicalNameCaps} returns the canonical capitalization for
|
||||
the given header.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertEqual(h._canonicalNameCaps(b"test"), b"Test")
|
||||
self.assertEqual(h._canonicalNameCaps(b"test-stuff"), b"Test-Stuff")
|
||||
self.assertEqual(h._canonicalNameCaps(b"content-md5"), b"Content-MD5")
|
||||
self.assertEqual(h._canonicalNameCaps(b"dnt"), b"DNT")
|
||||
self.assertEqual(h._canonicalNameCaps(b"etag"), b"ETag")
|
||||
self.assertEqual(h._canonicalNameCaps(b"p3p"), b"P3P")
|
||||
self.assertEqual(h._canonicalNameCaps(b"te"), b"TE")
|
||||
self.assertEqual(h._canonicalNameCaps(b"www-authenticate"),
|
||||
b"WWW-Authenticate")
|
||||
self.assertEqual(h._canonicalNameCaps(b"x-xss-protection"),
|
||||
b"X-XSS-Protection")
|
||||
|
||||
|
||||
def test_getAllRawHeaders(self):
|
||||
"""
|
||||
L{Headers.getAllRawHeaders} returns an iterable of (k, v) pairs, where
|
||||
C{k} is the canonicalized representation of the header name, and C{v}
|
||||
is a sequence of values.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(b"test", [b"lemurs"])
|
||||
h.setRawHeaders(b"www-authenticate", [b"basic aksljdlk="])
|
||||
|
||||
allHeaders = set([(k, tuple(v)) for k, v in h.getAllRawHeaders()])
|
||||
|
||||
self.assertEqual(allHeaders,
|
||||
set([(b"WWW-Authenticate", (b"basic aksljdlk=",)),
|
||||
(b"Test", (b"lemurs",))]))
|
||||
|
||||
|
||||
def test_headersComparison(self):
|
||||
"""
|
||||
A L{Headers} instance compares equal to itself and to another
|
||||
L{Headers} instance with the same values.
|
||||
"""
|
||||
first = Headers()
|
||||
first.setRawHeaders(b"foo", [b"panda"])
|
||||
second = Headers()
|
||||
second.setRawHeaders(b"foo", [b"panda"])
|
||||
third = Headers()
|
||||
third.setRawHeaders(b"foo", [b"lemur", b"panda"])
|
||||
self.assertEqual(first, first)
|
||||
self.assertEqual(first, second)
|
||||
self.assertNotEqual(first, third)
|
||||
|
||||
|
||||
def test_otherComparison(self):
|
||||
"""
|
||||
An instance of L{Headers} does not compare equal to other unrelated
|
||||
objects.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertNotEqual(h, ())
|
||||
self.assertNotEqual(h, object())
|
||||
self.assertNotEqual(h, b"foo")
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
The L{repr} of a L{Headers} instance shows the names and values of all
|
||||
the headers it contains.
|
||||
"""
|
||||
foo = b"foo"
|
||||
bar = b"bar"
|
||||
baz = b"baz"
|
||||
self.assertEqual(
|
||||
repr(Headers({foo: [bar, baz]})),
|
||||
"Headers({%r: [%r, %r]})" % (foo, bar, baz))
|
||||
|
||||
|
||||
def test_reprWithRawBytes(self):
|
||||
"""
|
||||
The L{repr} of a L{Headers} instance shows the names and values of all
|
||||
the headers it contains, not attempting to decode any raw bytes.
|
||||
"""
|
||||
# There's no such thing as undecodable latin-1, you'll just get
|
||||
# some mojibake
|
||||
foo = b"foo"
|
||||
# But this is invalid UTF-8! So, any accidental decoding/encoding will
|
||||
# throw an exception.
|
||||
bar = b"bar\xe1"
|
||||
baz = b"baz\xe1"
|
||||
self.assertEqual(
|
||||
repr(Headers({foo: [bar, baz]})),
|
||||
"Headers({%r: [%r, %r]})" % (foo, bar, baz))
|
||||
|
||||
|
||||
def test_subclassRepr(self):
|
||||
"""
|
||||
The L{repr} of an instance of a subclass of L{Headers} uses the name
|
||||
of the subclass instead of the string C{"Headers"}.
|
||||
"""
|
||||
foo = b"foo"
|
||||
bar = b"bar"
|
||||
baz = b"baz"
|
||||
class FunnyHeaders(Headers):
|
||||
pass
|
||||
self.assertEqual(
|
||||
repr(FunnyHeaders({foo: [bar, baz]})),
|
||||
"FunnyHeaders({%r: [%r, %r]})" % (foo, bar, baz))
|
||||
|
||||
|
||||
def test_copy(self):
|
||||
"""
|
||||
L{Headers.copy} creates a new independent copy of an existing
|
||||
L{Headers} instance, allowing future modifications without impacts
|
||||
between the copies.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(b'test', [b'foo'])
|
||||
i = h.copy()
|
||||
self.assertEqual(i.getRawHeaders(b'test'), [b'foo'])
|
||||
h.addRawHeader(b'test', b'bar')
|
||||
self.assertEqual(i.getRawHeaders(b'test'), [b'foo'])
|
||||
i.addRawHeader(b'test', b'baz')
|
||||
self.assertEqual(h.getRawHeaders(b'test'), [b'foo', b'bar'])
|
||||
|
||||
|
||||
|
||||
class UnicodeHeadersTests(TestCase):
|
||||
"""
|
||||
Tests for L{Headers}, using L{unicode} arguments for methods.
|
||||
"""
|
||||
|
||||
def test_sanitizeLinearWhitespace(self):
|
||||
"""
|
||||
Linear whitespace in header names or values is replaced with a
|
||||
single space.
|
||||
"""
|
||||
assertSanitized(self, textLinearWhitespaceComponents, sanitizedBytes)
|
||||
|
||||
|
||||
def test_initializer(self):
|
||||
"""
|
||||
The header values passed to L{Headers.__init__} can be retrieved via
|
||||
L{Headers.getRawHeaders}. If a L{bytes} argument is given, it returns
|
||||
L{bytes} values, and if a L{unicode} argument is given, it returns
|
||||
L{unicode} values. Both are the same header value, just encoded or
|
||||
decoded.
|
||||
"""
|
||||
h = Headers({u'Foo': [u'bar']})
|
||||
self.assertEqual(h.getRawHeaders(b'foo'), [b'bar'])
|
||||
self.assertEqual(h.getRawHeaders(u'foo'), [u'bar'])
|
||||
|
||||
|
||||
def test_setRawHeaders(self):
|
||||
"""
|
||||
L{Headers.setRawHeaders} sets the header values for the given
|
||||
header name to the sequence of strings, encoded.
|
||||
"""
|
||||
rawValue = [u"value1", u"value2"]
|
||||
rawEncodedValue = [b"value1", b"value2"]
|
||||
h = Headers()
|
||||
h.setRawHeaders("test", rawValue)
|
||||
self.assertTrue(h.hasHeader(b"test"))
|
||||
self.assertTrue(h.hasHeader(b"Test"))
|
||||
self.assertTrue(h.hasHeader("test"))
|
||||
self.assertTrue(h.hasHeader("Test"))
|
||||
self.assertEqual(h.getRawHeaders("test"), rawValue)
|
||||
self.assertEqual(h.getRawHeaders(b"test"), rawEncodedValue)
|
||||
|
||||
|
||||
def test_nameNotEncodable(self):
|
||||
"""
|
||||
Passing L{unicode} to any function that takes a header name will encode
|
||||
said header name as ISO-8859-1, and if it cannot be encoded, it will
|
||||
raise a L{UnicodeDecodeError}.
|
||||
"""
|
||||
h = Headers()
|
||||
|
||||
# Only these two functions take names
|
||||
with self.assertRaises(UnicodeEncodeError):
|
||||
h.setRawHeaders(u"\u2603", [u"val"])
|
||||
|
||||
with self.assertRaises(UnicodeEncodeError):
|
||||
h.hasHeader(u"\u2603")
|
||||
|
||||
|
||||
def test_nameEncoding(self):
|
||||
"""
|
||||
Passing L{unicode} to any function that takes a header name will encode
|
||||
said header name as ISO-8859-1.
|
||||
"""
|
||||
h = Headers()
|
||||
|
||||
# We set it using a Unicode string.
|
||||
h.setRawHeaders(u"\u00E1", [b"foo"])
|
||||
|
||||
# It's encoded to the ISO-8859-1 value, which we can use to access it
|
||||
self.assertTrue(h.hasHeader(b"\xe1"))
|
||||
self.assertEqual(h.getRawHeaders(b"\xe1"), [b'foo'])
|
||||
|
||||
# We can still access it using the Unicode string..
|
||||
self.assertTrue(h.hasHeader(u"\u00E1"))
|
||||
|
||||
|
||||
def test_rawHeadersValueEncoding(self):
|
||||
"""
|
||||
Passing L{unicode} to L{Headers.setRawHeaders} will encode the name as
|
||||
ISO-8859-1 and values as UTF-8.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u"\u00E1", [u"\u2603", b"foo"])
|
||||
self.assertTrue(h.hasHeader(b"\xe1"))
|
||||
self.assertEqual(h.getRawHeaders(b"\xe1"), [b'\xe2\x98\x83', b'foo'])
|
||||
|
||||
|
||||
def test_rawHeadersTypeChecking(self):
|
||||
"""
|
||||
L{Headers.setRawHeaders} requires values to be of type list.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertRaises(TypeError, h.setRawHeaders, u'key', {u'Foo': u'bar'})
|
||||
|
||||
|
||||
def test_addRawHeader(self):
|
||||
"""
|
||||
L{Headers.addRawHeader} adds a new value for a given header.
|
||||
"""
|
||||
h = Headers()
|
||||
h.addRawHeader(u"test", u"lemur")
|
||||
self.assertEqual(h.getRawHeaders(u"test"), [u"lemur"])
|
||||
h.addRawHeader(u"test", u"panda")
|
||||
self.assertEqual(h.getRawHeaders(u"test"), [u"lemur", u"panda"])
|
||||
self.assertEqual(h.getRawHeaders(b"test"), [b"lemur", b"panda"])
|
||||
|
||||
|
||||
def test_getRawHeadersNoDefault(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns L{None} if the header is not found and
|
||||
no default is specified.
|
||||
"""
|
||||
self.assertIsNone(Headers().getRawHeaders(u"test"))
|
||||
|
||||
|
||||
def test_getRawHeadersDefaultValue(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns the specified default value when no
|
||||
header is found.
|
||||
"""
|
||||
h = Headers()
|
||||
default = object()
|
||||
self.assertIdentical(h.getRawHeaders(u"test", default), default)
|
||||
self.assertIdentical(h.getRawHeaders(u"test", None), None)
|
||||
self.assertEqual(h.getRawHeaders(u"test", [None]), [None])
|
||||
self.assertEqual(
|
||||
h.getRawHeaders(u"test", [u"\N{SNOWMAN}"]),
|
||||
[u"\N{SNOWMAN}"],
|
||||
)
|
||||
|
||||
|
||||
def test_getRawHeadersWithDefaultMatchingValue(self):
|
||||
"""
|
||||
If the object passed as the value list to L{Headers.setRawHeaders}
|
||||
is later passed as a default to L{Headers.getRawHeaders}, the
|
||||
result nevertheless contains decoded values.
|
||||
"""
|
||||
h = Headers()
|
||||
default = [b"value"]
|
||||
h.setRawHeaders(b"key", default)
|
||||
self.assertIsInstance(h.getRawHeaders(u"key", default)[0], unicode)
|
||||
self.assertEqual(h.getRawHeaders(u"key", default), [u"value"])
|
||||
|
||||
|
||||
def test_getRawHeaders(self):
|
||||
"""
|
||||
L{Headers.getRawHeaders} returns the values which have been set for a
|
||||
given header.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u"test\u00E1", [u"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(u"test\u00E1"), [u"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(u"Test\u00E1"), [u"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(b"test\xe1"), [b"lemur"])
|
||||
self.assertEqual(h.getRawHeaders(b"Test\xe1"), [b"lemur"])
|
||||
|
||||
|
||||
def test_hasHeaderTrue(self):
|
||||
"""
|
||||
Check that L{Headers.hasHeader} returns C{True} when the given header
|
||||
is found.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u"test\u00E1", [u"lemur"])
|
||||
self.assertTrue(h.hasHeader(u"test\u00E1"))
|
||||
self.assertTrue(h.hasHeader(u"Test\u00E1"))
|
||||
self.assertTrue(h.hasHeader(b"test\xe1"))
|
||||
self.assertTrue(h.hasHeader(b"Test\xe1"))
|
||||
|
||||
|
||||
def test_hasHeaderFalse(self):
|
||||
"""
|
||||
L{Headers.hasHeader} returns C{False} when the given header is not
|
||||
found.
|
||||
"""
|
||||
self.assertFalse(Headers().hasHeader(u"test\u00E1"))
|
||||
|
||||
|
||||
def test_removeHeader(self):
|
||||
"""
|
||||
Check that L{Headers.removeHeader} removes the given header.
|
||||
"""
|
||||
h = Headers()
|
||||
|
||||
h.setRawHeaders(u"foo", [u"lemur"])
|
||||
self.assertTrue(h.hasHeader(u"foo"))
|
||||
h.removeHeader(u"foo")
|
||||
self.assertFalse(h.hasHeader(u"foo"))
|
||||
self.assertFalse(h.hasHeader(b"foo"))
|
||||
|
||||
h.setRawHeaders(u"bar", [u"panda"])
|
||||
self.assertTrue(h.hasHeader(u"bar"))
|
||||
h.removeHeader(u"Bar")
|
||||
self.assertFalse(h.hasHeader(u"bar"))
|
||||
self.assertFalse(h.hasHeader(b"bar"))
|
||||
|
||||
|
||||
def test_removeHeaderDoesntExist(self):
|
||||
"""
|
||||
L{Headers.removeHeader} is a no-operation when the specified header is
|
||||
not found.
|
||||
"""
|
||||
h = Headers()
|
||||
h.removeHeader(u"test")
|
||||
self.assertEqual(list(h.getAllRawHeaders()), [])
|
||||
|
||||
|
||||
def test_getAllRawHeaders(self):
|
||||
"""
|
||||
L{Headers.getAllRawHeaders} returns an iterable of (k, v) pairs, where
|
||||
C{k} is the canonicalized representation of the header name, and C{v}
|
||||
is a sequence of values.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u"test\u00E1", [u"lemurs"])
|
||||
h.setRawHeaders(u"www-authenticate", [u"basic aksljdlk="])
|
||||
h.setRawHeaders(u"content-md5", [u"kjdfdfgdfgnsd"])
|
||||
|
||||
allHeaders = set([(k, tuple(v)) for k, v in h.getAllRawHeaders()])
|
||||
|
||||
self.assertEqual(allHeaders,
|
||||
set([(b"WWW-Authenticate", (b"basic aksljdlk=",)),
|
||||
(b"Content-MD5", (b"kjdfdfgdfgnsd",)),
|
||||
(b"Test\xe1", (b"lemurs",))]))
|
||||
|
||||
|
||||
def test_headersComparison(self):
|
||||
"""
|
||||
A L{Headers} instance compares equal to itself and to another
|
||||
L{Headers} instance with the same values.
|
||||
"""
|
||||
first = Headers()
|
||||
first.setRawHeaders(u"foo\u00E1", [u"panda"])
|
||||
second = Headers()
|
||||
second.setRawHeaders(u"foo\u00E1", [u"panda"])
|
||||
third = Headers()
|
||||
third.setRawHeaders(u"foo\u00E1", [u"lemur", u"panda"])
|
||||
|
||||
self.assertEqual(first, first)
|
||||
self.assertEqual(first, second)
|
||||
self.assertNotEqual(first, third)
|
||||
|
||||
# Headers instantiated with bytes equivs are also the same
|
||||
firstBytes = Headers()
|
||||
firstBytes.setRawHeaders(b"foo\xe1", [b"panda"])
|
||||
secondBytes = Headers()
|
||||
secondBytes.setRawHeaders(b"foo\xe1", [b"panda"])
|
||||
thirdBytes = Headers()
|
||||
thirdBytes.setRawHeaders(b"foo\xe1", [b"lemur", u"panda"])
|
||||
|
||||
self.assertEqual(first, firstBytes)
|
||||
self.assertEqual(second, secondBytes)
|
||||
self.assertEqual(third, thirdBytes)
|
||||
|
||||
|
||||
def test_otherComparison(self):
|
||||
"""
|
||||
An instance of L{Headers} does not compare equal to other unrelated
|
||||
objects.
|
||||
"""
|
||||
h = Headers()
|
||||
self.assertNotEqual(h, ())
|
||||
self.assertNotEqual(h, object())
|
||||
self.assertNotEqual(h, u"foo")
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
The L{repr} of a L{Headers} instance shows the names and values of all
|
||||
the headers it contains. This shows only reprs of bytes values, as
|
||||
undecodable headers may cause an exception.
|
||||
"""
|
||||
foo = u"foo\u00E1"
|
||||
bar = u"bar\u2603"
|
||||
baz = u"baz"
|
||||
fooEncoded = "'foo\\xe1'"
|
||||
barEncoded = "'bar\\xe2\\x98\\x83'"
|
||||
if _PY3:
|
||||
fooEncoded = "b" + fooEncoded
|
||||
barEncoded = "b" + barEncoded
|
||||
self.assertEqual(
|
||||
repr(Headers({foo: [bar, baz]})),
|
||||
"Headers({%s: [%s, %r]})" % (fooEncoded,
|
||||
barEncoded,
|
||||
baz.encode('utf8')))
|
||||
|
||||
|
||||
def test_subclassRepr(self):
|
||||
"""
|
||||
The L{repr} of an instance of a subclass of L{Headers} uses the name
|
||||
of the subclass instead of the string C{"Headers"}.
|
||||
"""
|
||||
foo = u"foo\u00E1"
|
||||
bar = u"bar\u2603"
|
||||
baz = u"baz"
|
||||
fooEncoded = "'foo\\xe1'"
|
||||
barEncoded = "'bar\\xe2\\x98\\x83'"
|
||||
if _PY3:
|
||||
fooEncoded = "b" + fooEncoded
|
||||
barEncoded = "b" + barEncoded
|
||||
class FunnyHeaders(Headers):
|
||||
pass
|
||||
self.assertEqual(
|
||||
repr(FunnyHeaders({foo: [bar, baz]})),
|
||||
"FunnyHeaders({%s: [%s, %r]})" % (fooEncoded,
|
||||
barEncoded,
|
||||
baz.encode('utf8')))
|
||||
|
||||
|
||||
def test_copy(self):
|
||||
"""
|
||||
L{Headers.copy} creates a new independent copy of an existing
|
||||
L{Headers} instance, allowing future modifications without impacts
|
||||
between the copies.
|
||||
"""
|
||||
h = Headers()
|
||||
h.setRawHeaders(u'test\u00E1', [u'foo\u2603'])
|
||||
i = h.copy()
|
||||
|
||||
# The copy contains the same value as the original
|
||||
self.assertEqual(i.getRawHeaders(u'test\u00E1'), [u'foo\u2603'])
|
||||
self.assertEqual(i.getRawHeaders(b'test\xe1'), [b'foo\xe2\x98\x83'])
|
||||
|
||||
# Add a header to the original
|
||||
h.addRawHeader(u'test\u00E1', u'bar')
|
||||
|
||||
# Verify that the copy has not changed
|
||||
self.assertEqual(i.getRawHeaders(u'test\u00E1'), [u'foo\u2603'])
|
||||
self.assertEqual(i.getRawHeaders(b'test\xe1'), [b'foo\xe2\x98\x83'])
|
||||
|
||||
# Add a header to the copy
|
||||
i.addRawHeader(u'test\u00E1', b'baz')
|
||||
|
||||
# Verify that the orignal does not have it
|
||||
self.assertEqual(
|
||||
h.getRawHeaders(u'test\u00E1'), [u'foo\u2603', u'bar'])
|
||||
self.assertEqual(
|
||||
h.getRawHeaders(b'test\xe1'), [b'foo\xe2\x98\x83', b'bar'])
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,289 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.web.resource}.
|
||||
"""
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.python.compat import _PY3
|
||||
|
||||
from twisted.web.error import UnsupportedMethod
|
||||
from twisted.web.resource import (
|
||||
NOT_FOUND, FORBIDDEN, Resource, ErrorPage, NoResource, ForbiddenResource,
|
||||
getChildForRequest)
|
||||
from twisted.web.http_headers import Headers
|
||||
from twisted.web.test.requesthelper import DummyRequest
|
||||
|
||||
|
||||
class ErrorPageTests(TestCase):
|
||||
"""
|
||||
Tests for L{ErrorPage}, L{NoResource}, and L{ForbiddenResource}.
|
||||
"""
|
||||
|
||||
errorPage = ErrorPage
|
||||
noResource = NoResource
|
||||
forbiddenResource = ForbiddenResource
|
||||
|
||||
def test_getChild(self):
|
||||
"""
|
||||
The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
|
||||
called on.
|
||||
"""
|
||||
page = self.errorPage(321, "foo", "bar")
|
||||
self.assertIdentical(page.getChild(b"name", object()), page)
|
||||
|
||||
|
||||
def _pageRenderingTest(self, page, code, brief, detail):
|
||||
request = DummyRequest([b''])
|
||||
template = (
|
||||
u"\n"
|
||||
u"<html>\n"
|
||||
u" <head><title>%s - %s</title></head>\n"
|
||||
u" <body>\n"
|
||||
u" <h1>%s</h1>\n"
|
||||
u" <p>%s</p>\n"
|
||||
u" </body>\n"
|
||||
u"</html>\n")
|
||||
expected = template % (code, brief, brief, detail)
|
||||
self.assertEqual(
|
||||
page.render(request), expected.encode('utf-8'))
|
||||
self.assertEqual(request.responseCode, code)
|
||||
self.assertEqual(
|
||||
request.responseHeaders,
|
||||
Headers({b'content-type': [b'text/html; charset=utf-8']}))
|
||||
|
||||
|
||||
def test_errorPageRendering(self):
|
||||
"""
|
||||
L{ErrorPage.render} returns a C{bytes} describing the error defined by
|
||||
the response code and message passed to L{ErrorPage.__init__}. It also
|
||||
uses that response code to set the response code on the L{Request}
|
||||
passed in.
|
||||
"""
|
||||
code = 321
|
||||
brief = "brief description text"
|
||||
detail = "much longer text might go here"
|
||||
page = self.errorPage(code, brief, detail)
|
||||
self._pageRenderingTest(page, code, brief, detail)
|
||||
|
||||
|
||||
def test_noResourceRendering(self):
|
||||
"""
|
||||
L{NoResource} sets the HTTP I{NOT FOUND} code.
|
||||
"""
|
||||
detail = "long message"
|
||||
page = self.noResource(detail)
|
||||
self._pageRenderingTest(page, NOT_FOUND, "No Such Resource", detail)
|
||||
|
||||
|
||||
def test_forbiddenResourceRendering(self):
|
||||
"""
|
||||
L{ForbiddenResource} sets the HTTP I{FORBIDDEN} code.
|
||||
"""
|
||||
detail = "longer message"
|
||||
page = self.forbiddenResource(detail)
|
||||
self._pageRenderingTest(page, FORBIDDEN, "Forbidden Resource", detail)
|
||||
|
||||
|
||||
|
||||
class DynamicChild(Resource):
|
||||
"""
|
||||
A L{Resource} to be created on the fly by L{DynamicChildren}.
|
||||
"""
|
||||
def __init__(self, path, request):
|
||||
Resource.__init__(self)
|
||||
self.path = path
|
||||
self.request = request
|
||||
|
||||
|
||||
|
||||
class DynamicChildren(Resource):
|
||||
"""
|
||||
A L{Resource} with dynamic children.
|
||||
"""
|
||||
def getChild(self, path, request):
|
||||
return DynamicChild(path, request)
|
||||
|
||||
|
||||
|
||||
class BytesReturnedRenderable(Resource):
|
||||
"""
|
||||
A L{Resource} with minimal capabilities to render a response.
|
||||
"""
|
||||
def __init__(self, response):
|
||||
"""
|
||||
@param response: A C{bytes} object giving the value to return from
|
||||
C{render_GET}.
|
||||
"""
|
||||
Resource.__init__(self)
|
||||
self._response = response
|
||||
|
||||
|
||||
def render_GET(self, request):
|
||||
"""
|
||||
Render a response to a I{GET} request by returning a short byte string
|
||||
to be written by the server.
|
||||
"""
|
||||
return self._response
|
||||
|
||||
|
||||
|
||||
class ImplicitAllowedMethods(Resource):
|
||||
"""
|
||||
A L{Resource} which implicitly defines its allowed methods by defining
|
||||
renderers to handle them.
|
||||
"""
|
||||
def render_GET(self, request):
|
||||
pass
|
||||
|
||||
|
||||
def render_PUT(self, request):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class ResourceTests(TestCase):
|
||||
"""
|
||||
Tests for L{Resource}.
|
||||
"""
|
||||
def test_staticChildren(self):
|
||||
"""
|
||||
L{Resource.putChild} adds a I{static} child to the resource. That child
|
||||
is returned from any call to L{Resource.getChildWithDefault} for the
|
||||
child's path.
|
||||
"""
|
||||
resource = Resource()
|
||||
child = Resource()
|
||||
sibling = Resource()
|
||||
resource.putChild(b"foo", child)
|
||||
resource.putChild(b"bar", sibling)
|
||||
self.assertIdentical(
|
||||
child, resource.getChildWithDefault(b"foo", DummyRequest([])))
|
||||
|
||||
|
||||
def test_dynamicChildren(self):
|
||||
"""
|
||||
L{Resource.getChildWithDefault} delegates to L{Resource.getChild} when
|
||||
the requested path is not associated with any static child.
|
||||
"""
|
||||
path = b"foo"
|
||||
request = DummyRequest([])
|
||||
resource = DynamicChildren()
|
||||
child = resource.getChildWithDefault(path, request)
|
||||
self.assertIsInstance(child, DynamicChild)
|
||||
self.assertEqual(child.path, path)
|
||||
self.assertIdentical(child.request, request)
|
||||
|
||||
|
||||
def test_staticChildPathType(self):
|
||||
"""
|
||||
Test that passing the wrong type to putChild results in a warning,
|
||||
and a failure in Python 3
|
||||
"""
|
||||
resource = Resource()
|
||||
child = Resource()
|
||||
sibling = Resource()
|
||||
resource.putChild(u"foo", child)
|
||||
warnings = self.flushWarnings([self.test_staticChildPathType])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertIn("Path segment must be bytes",
|
||||
warnings[0]['message'])
|
||||
if _PY3:
|
||||
# We expect an error here because u"foo" != b"foo" on Py3k
|
||||
self.assertIsInstance(
|
||||
resource.getChildWithDefault(b"foo", DummyRequest([])),
|
||||
ErrorPage)
|
||||
|
||||
resource.putChild(None, sibling)
|
||||
warnings = self.flushWarnings([self.test_staticChildPathType])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertIn("Path segment must be bytes",
|
||||
warnings[0]['message'])
|
||||
|
||||
|
||||
def test_defaultHEAD(self):
|
||||
"""
|
||||
When not otherwise overridden, L{Resource.render} treats a I{HEAD}
|
||||
request as if it were a I{GET} request.
|
||||
"""
|
||||
expected = b"insert response here"
|
||||
request = DummyRequest([])
|
||||
request.method = b'HEAD'
|
||||
resource = BytesReturnedRenderable(expected)
|
||||
self.assertEqual(expected, resource.render(request))
|
||||
|
||||
|
||||
def test_explicitAllowedMethods(self):
|
||||
"""
|
||||
The L{UnsupportedMethod} raised by L{Resource.render} for an unsupported
|
||||
request method has a C{allowedMethods} attribute set to the value of the
|
||||
C{allowedMethods} attribute of the L{Resource}, if it has one.
|
||||
"""
|
||||
expected = [b'GET', b'HEAD', b'PUT']
|
||||
resource = Resource()
|
||||
resource.allowedMethods = expected
|
||||
request = DummyRequest([])
|
||||
request.method = b'FICTIONAL'
|
||||
exc = self.assertRaises(UnsupportedMethod, resource.render, request)
|
||||
self.assertEqual(set(expected), set(exc.allowedMethods))
|
||||
|
||||
|
||||
def test_implicitAllowedMethods(self):
|
||||
"""
|
||||
The L{UnsupportedMethod} raised by L{Resource.render} for an unsupported
|
||||
request method has a C{allowedMethods} attribute set to a list of the
|
||||
methods supported by the L{Resource}, as determined by the
|
||||
I{render_}-prefixed methods which it defines, if C{allowedMethods} is
|
||||
not explicitly defined by the L{Resource}.
|
||||
"""
|
||||
expected = set([b'GET', b'HEAD', b'PUT'])
|
||||
resource = ImplicitAllowedMethods()
|
||||
request = DummyRequest([])
|
||||
request.method = b'FICTIONAL'
|
||||
exc = self.assertRaises(UnsupportedMethod, resource.render, request)
|
||||
self.assertEqual(expected, set(exc.allowedMethods))
|
||||
|
||||
|
||||
|
||||
|
||||
class GetChildForRequestTests(TestCase):
|
||||
"""
|
||||
Tests for L{getChildForRequest}.
|
||||
"""
|
||||
def test_exhaustedPostPath(self):
|
||||
"""
|
||||
L{getChildForRequest} returns whatever resource has been reached by the
|
||||
time the request's C{postpath} is empty.
|
||||
"""
|
||||
request = DummyRequest([])
|
||||
resource = Resource()
|
||||
result = getChildForRequest(resource, request)
|
||||
self.assertIdentical(resource, result)
|
||||
|
||||
|
||||
def test_leafResource(self):
|
||||
"""
|
||||
L{getChildForRequest} returns the first resource it encounters with a
|
||||
C{isLeaf} attribute set to C{True}.
|
||||
"""
|
||||
request = DummyRequest([b"foo", b"bar"])
|
||||
resource = Resource()
|
||||
resource.isLeaf = True
|
||||
result = getChildForRequest(resource, request)
|
||||
self.assertIdentical(resource, result)
|
||||
|
||||
|
||||
def test_postPathToPrePath(self):
|
||||
"""
|
||||
As path segments from the request are traversed, they are taken from
|
||||
C{postpath} and put into C{prepath}.
|
||||
"""
|
||||
request = DummyRequest([b"foo", b"bar"])
|
||||
root = Resource()
|
||||
child = Resource()
|
||||
child.isLeaf = True
|
||||
root.putChild(b"foo", child)
|
||||
self.assertIdentical(child, getChildForRequest(root, request))
|
||||
self.assertEqual(request.prepath, [b"foo"])
|
||||
self.assertEqual(request.postpath, [b"bar"])
|
||||
@@ -0,0 +1,200 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.web.vhost}.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
|
||||
from twisted.internet.defer import gatherResults
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.web.http import NOT_FOUND
|
||||
from twisted.web.resource import NoResource
|
||||
from twisted.web.static import Data
|
||||
from twisted.web.server import Site
|
||||
from twisted.web.vhost import (_HostResource,
|
||||
NameVirtualHost,
|
||||
VHostMonsterResource)
|
||||
from twisted.web.test.test_web import DummyRequest
|
||||
from twisted.web.test._util import _render
|
||||
|
||||
|
||||
class HostResourceTests(TestCase):
|
||||
"""
|
||||
Tests for L{_HostResource}.
|
||||
"""
|
||||
def test_getChild(self):
|
||||
"""
|
||||
L{_HostResource.getChild} returns the proper I{Resource} for the vhost
|
||||
embedded in the URL. Verify that returning the proper I{Resource}
|
||||
required changing the I{Host} in the header.
|
||||
"""
|
||||
bazroot = Data(b'root data', "")
|
||||
bazuri = Data(b'uri data', "")
|
||||
baztest = Data(b'test data', "")
|
||||
bazuri.putChild(b'test', baztest)
|
||||
bazroot.putChild(b'uri', bazuri)
|
||||
hr = _HostResource()
|
||||
|
||||
root = NameVirtualHost()
|
||||
root.default = Data(b'default data', "")
|
||||
root.addHost(b'baz.com', bazroot)
|
||||
|
||||
request = DummyRequest([b'uri', b'test'])
|
||||
request.prepath = [b'bar', b'http', b'baz.com']
|
||||
request.site = Site(root)
|
||||
request.isSecure = lambda: False
|
||||
request.host = b''
|
||||
|
||||
step = hr.getChild(b'baz.com', request) # Consumes rest of path
|
||||
self.assertIsInstance(step, Data)
|
||||
|
||||
request = DummyRequest([b'uri', b'test'])
|
||||
step = root.getChild(b'uri', request)
|
||||
self.assertIsInstance(step, NoResource)
|
||||
|
||||
|
||||
|
||||
class NameVirtualHostTests(TestCase):
|
||||
"""
|
||||
Tests for L{NameVirtualHost}.
|
||||
"""
|
||||
def test_renderWithoutHost(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns the result of rendering the
|
||||
instance's C{default} if it is not L{None} and there is no I{Host}
|
||||
header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
virtualHostResource.default = Data(b"correct result", "")
|
||||
request = DummyRequest([''])
|
||||
self.assertEqual(
|
||||
virtualHostResource.render(request), b"correct result")
|
||||
|
||||
|
||||
def test_renderWithoutHostNoDefault(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns a response with a status of I{NOT
|
||||
FOUND} if the instance's C{default} is L{None} and there is no I{Host}
|
||||
header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
request = DummyRequest([''])
|
||||
d = _render(virtualHostResource, request)
|
||||
def cbRendered(ignored):
|
||||
self.assertEqual(request.responseCode, NOT_FOUND)
|
||||
d.addCallback(cbRendered)
|
||||
return d
|
||||
|
||||
|
||||
def test_renderWithHost(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns the result of rendering the resource
|
||||
which is the value in the instance's C{host} dictionary corresponding
|
||||
to the key indicated by the value of the I{Host} header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
virtualHostResource.addHost(b'example.org', Data(b"winner", ""))
|
||||
|
||||
request = DummyRequest([b''])
|
||||
request.requestHeaders.addRawHeader(b'host', b'example.org')
|
||||
d = _render(virtualHostResource, request)
|
||||
def cbRendered(ignored, request):
|
||||
self.assertEqual(b''.join(request.written), b"winner")
|
||||
d.addCallback(cbRendered, request)
|
||||
|
||||
# The port portion of the Host header should not be considered.
|
||||
requestWithPort = DummyRequest([b''])
|
||||
requestWithPort.requestHeaders.addRawHeader(b'host', b'example.org:8000')
|
||||
dWithPort = _render(virtualHostResource, requestWithPort)
|
||||
def cbRendered(ignored, requestWithPort):
|
||||
self.assertEqual(b''.join(requestWithPort.written), b"winner")
|
||||
dWithPort.addCallback(cbRendered, requestWithPort)
|
||||
|
||||
return gatherResults([d, dWithPort])
|
||||
|
||||
|
||||
def test_renderWithUnknownHost(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns the result of rendering the
|
||||
instance's C{default} if it is not L{None} and there is no host
|
||||
matching the value of the I{Host} header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
virtualHostResource.default = Data(b"correct data", "")
|
||||
request = DummyRequest([b''])
|
||||
request.requestHeaders.addRawHeader(b'host', b'example.com')
|
||||
d = _render(virtualHostResource, request)
|
||||
def cbRendered(ignored):
|
||||
self.assertEqual(b''.join(request.written), b"correct data")
|
||||
d.addCallback(cbRendered)
|
||||
return d
|
||||
|
||||
|
||||
def test_renderWithUnknownHostNoDefault(self):
|
||||
"""
|
||||
L{NameVirtualHost.render} returns a response with a status of I{NOT
|
||||
FOUND} if the instance's C{default} is L{None} and there is no host
|
||||
matching the value of the I{Host} header in the request.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
request = DummyRequest([''])
|
||||
request.requestHeaders.addRawHeader(b'host', b'example.com')
|
||||
d = _render(virtualHostResource, request)
|
||||
def cbRendered(ignored):
|
||||
self.assertEqual(request.responseCode, NOT_FOUND)
|
||||
d.addCallback(cbRendered)
|
||||
return d
|
||||
|
||||
|
||||
def test_getChild(self):
|
||||
"""
|
||||
L{NameVirtualHost.getChild} returns correct I{Resource} based off
|
||||
the header and modifies I{Request} to ensure proper prepath and
|
||||
postpath are set.
|
||||
"""
|
||||
virtualHostResource = NameVirtualHost()
|
||||
leafResource = Data(b"leaf data", "")
|
||||
leafResource.isLeaf = True
|
||||
normResource = Data(b"norm data", "")
|
||||
virtualHostResource.addHost(b'leaf.example.org', leafResource)
|
||||
virtualHostResource.addHost(b'norm.example.org', normResource)
|
||||
|
||||
request = DummyRequest([])
|
||||
request.requestHeaders.addRawHeader(b'host', b'norm.example.org')
|
||||
request.prepath = [b'']
|
||||
|
||||
self.assertIsInstance(virtualHostResource.getChild(b'', request),
|
||||
NoResource)
|
||||
self.assertEqual(request.prepath, [b''])
|
||||
self.assertEqual(request.postpath, [])
|
||||
|
||||
request = DummyRequest([])
|
||||
request.requestHeaders.addRawHeader(b'host', b'leaf.example.org')
|
||||
request.prepath = [b'']
|
||||
|
||||
self.assertIsInstance(virtualHostResource.getChild(b'', request),
|
||||
Data)
|
||||
self.assertEqual(request.prepath, [])
|
||||
self.assertEqual(request.postpath, [b''])
|
||||
|
||||
|
||||
|
||||
class VHostMonsterResourceTests(TestCase):
|
||||
"""
|
||||
Tests for L{VHostMonsterResource}.
|
||||
"""
|
||||
def test_getChild(self):
|
||||
"""
|
||||
L{VHostMonsterResource.getChild} returns I{_HostResource} and modifies
|
||||
I{Request} with correct L{Request.isSecure}.
|
||||
"""
|
||||
vhm = VHostMonsterResource()
|
||||
request = DummyRequest([])
|
||||
self.assertIsInstance(vhm.getChild(b'http', request), _HostResource)
|
||||
self.assertFalse(request.isSecure())
|
||||
|
||||
request = DummyRequest([])
|
||||
self.assertIsInstance(vhm.getChild(b'https', request), _HostResource)
|
||||
self.assertTrue(request.isSecure())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
The L{_response} module contains constants for all standard HTTP codes, along
|
||||
with a mapping to the corresponding phrases.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
import string
|
||||
from twisted.trial import unittest
|
||||
from twisted.web import _responses
|
||||
|
||||
class ResponseTests(unittest.TestCase):
|
||||
|
||||
def test_constants(self):
|
||||
"""
|
||||
All constants besides C{RESPONSES} defined in L{_response} are
|
||||
integers and are keys in C{RESPONSES}.
|
||||
"""
|
||||
for sym in dir(_responses):
|
||||
if sym == 'RESPONSES':
|
||||
continue
|
||||
if all((c == '_' or c in string.ascii_uppercase) for c in sym):
|
||||
val = getattr(_responses, sym)
|
||||
self.assertIsInstance(val, int)
|
||||
self.assertIn(val, _responses.RESPONSES)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,915 @@
|
||||
# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for XML-RPC support in L{twisted.web.xmlrpc}.
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.python.compat import nativeString, networkString, NativeStringIO
|
||||
from io import BytesIO
|
||||
|
||||
import datetime
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.web import xmlrpc
|
||||
from twisted.web.xmlrpc import XMLRPC, payloadTemplate, addIntrospection
|
||||
from twisted.web.xmlrpc import _QueryFactory, withRequest, xmlrpclib
|
||||
from twisted.web import server, client, http, static
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet.error import ConnectionDone
|
||||
from twisted.python import failure
|
||||
from twisted.python.reflect import namedModule
|
||||
from twisted.test.proto_helpers import MemoryReactor, EventLoggingObserver
|
||||
from twisted.web.test.test_web import DummyRequest
|
||||
from twisted.logger import (globalLogPublisher, FilteringLogObserver,
|
||||
LogLevelFilterPredicate, LogLevel)
|
||||
try:
|
||||
namedModule('twisted.internet.ssl')
|
||||
except ImportError:
|
||||
sslSkip = "OpenSSL not present"
|
||||
else:
|
||||
sslSkip = None
|
||||
|
||||
|
||||
class AsyncXMLRPCTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{XMLRPC}'s support of Deferreds.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.request = DummyRequest([''])
|
||||
self.request.method = 'POST'
|
||||
self.request.content = NativeStringIO(
|
||||
payloadTemplate % ('async', xmlrpclib.dumps(())))
|
||||
|
||||
result = self.result = defer.Deferred()
|
||||
class AsyncResource(XMLRPC):
|
||||
def xmlrpc_async(self):
|
||||
return result
|
||||
|
||||
self.resource = AsyncResource()
|
||||
|
||||
|
||||
def test_deferredResponse(self):
|
||||
"""
|
||||
If an L{XMLRPC} C{xmlrpc_*} method returns a L{defer.Deferred}, the
|
||||
response to the request is the result of that L{defer.Deferred}.
|
||||
"""
|
||||
self.resource.render(self.request)
|
||||
self.assertEqual(self.request.written, [])
|
||||
|
||||
self.result.callback("result")
|
||||
|
||||
resp = xmlrpclib.loads(b"".join(self.request.written))
|
||||
self.assertEqual(resp, (('result',), None))
|
||||
self.assertEqual(self.request.finished, 1)
|
||||
|
||||
|
||||
def test_interruptedDeferredResponse(self):
|
||||
"""
|
||||
While waiting for the L{Deferred} returned by an L{XMLRPC} C{xmlrpc_*}
|
||||
method to fire, the connection the request was issued over may close.
|
||||
If this happens, neither C{write} nor C{finish} is called on the
|
||||
request.
|
||||
"""
|
||||
self.resource.render(self.request)
|
||||
self.request.processingFailed(
|
||||
failure.Failure(ConnectionDone("Simulated")))
|
||||
self.result.callback("result")
|
||||
self.assertEqual(self.request.written, [])
|
||||
self.assertEqual(self.request.finished, 0)
|
||||
|
||||
|
||||
|
||||
class TestRuntimeError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TestValueError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Test(XMLRPC):
|
||||
|
||||
# If you add xmlrpc_ methods to this class, go change test_listMethods
|
||||
# below.
|
||||
|
||||
FAILURE = 666
|
||||
NOT_FOUND = 23
|
||||
SESSION_EXPIRED = 42
|
||||
|
||||
def xmlrpc_echo(self, arg):
|
||||
return arg
|
||||
|
||||
# the doc string is part of the test
|
||||
def xmlrpc_add(self, a, b):
|
||||
"""
|
||||
This function add two numbers.
|
||||
"""
|
||||
return a + b
|
||||
|
||||
xmlrpc_add.signature = [['int', 'int', 'int'],
|
||||
['double', 'double', 'double']]
|
||||
|
||||
# the doc string is part of the test
|
||||
def xmlrpc_pair(self, string, num):
|
||||
"""
|
||||
This function puts the two arguments in an array.
|
||||
"""
|
||||
return [string, num]
|
||||
|
||||
xmlrpc_pair.signature = [['array', 'string', 'int']]
|
||||
|
||||
# the doc string is part of the test
|
||||
def xmlrpc_defer(self, x):
|
||||
"""Help for defer."""
|
||||
return defer.succeed(x)
|
||||
|
||||
def xmlrpc_deferFail(self):
|
||||
return defer.fail(TestValueError())
|
||||
|
||||
# don't add a doc string, it's part of the test
|
||||
def xmlrpc_fail(self):
|
||||
raise TestRuntimeError
|
||||
|
||||
def xmlrpc_fault(self):
|
||||
return xmlrpc.Fault(12, "hello")
|
||||
|
||||
def xmlrpc_deferFault(self):
|
||||
return defer.fail(xmlrpc.Fault(17, "hi"))
|
||||
|
||||
def xmlrpc_snowman(self, payload):
|
||||
"""
|
||||
Used to test that we can pass Unicode.
|
||||
"""
|
||||
snowman = u"\u2603"
|
||||
if snowman != payload:
|
||||
return xmlrpc.Fault(13, "Payload not unicode snowman")
|
||||
return snowman
|
||||
|
||||
def xmlrpc_complex(self):
|
||||
return {"a": ["b", "c", 12, []], "D": "foo"}
|
||||
|
||||
def xmlrpc_dict(self, map, key):
|
||||
return map[key]
|
||||
xmlrpc_dict.help = 'Help for dict.'
|
||||
|
||||
@withRequest
|
||||
def xmlrpc_withRequest(self, request, other):
|
||||
"""
|
||||
A method decorated with L{withRequest} which can be called by
|
||||
a test to verify that the request object really is passed as
|
||||
an argument.
|
||||
"""
|
||||
return (
|
||||
# as a proof that request is a request
|
||||
request.method +
|
||||
# plus proof other arguments are still passed along
|
||||
' ' + other)
|
||||
|
||||
|
||||
def lookupProcedure(self, procedurePath):
|
||||
try:
|
||||
return XMLRPC.lookupProcedure(self, procedurePath)
|
||||
except xmlrpc.NoSuchFunction:
|
||||
if procedurePath.startswith("SESSION"):
|
||||
raise xmlrpc.Fault(self.SESSION_EXPIRED,
|
||||
"Session non-existent/expired.")
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
|
||||
class TestLookupProcedure(XMLRPC):
|
||||
"""
|
||||
This is a resource which customizes procedure lookup to be used by the tests
|
||||
of support for this customization.
|
||||
"""
|
||||
def echo(self, x):
|
||||
return x
|
||||
|
||||
|
||||
def lookupProcedure(self, procedureName):
|
||||
"""
|
||||
Lookup a procedure from a fixed set of choices, either I{echo} or
|
||||
I{system.listeMethods}.
|
||||
"""
|
||||
if procedureName == 'echo':
|
||||
return self.echo
|
||||
raise xmlrpc.NoSuchFunction(
|
||||
self.NOT_FOUND, 'procedure %s not found' % (procedureName,))
|
||||
|
||||
|
||||
|
||||
class TestListProcedures(XMLRPC):
|
||||
"""
|
||||
This is a resource which customizes procedure enumeration to be used by the
|
||||
tests of support for this customization.
|
||||
"""
|
||||
def listProcedures(self):
|
||||
"""
|
||||
Return a list of a single method this resource will claim to support.
|
||||
"""
|
||||
return ['foo']
|
||||
|
||||
|
||||
|
||||
class TestAuthHeader(Test):
|
||||
"""
|
||||
This is used to get the header info so that we can test
|
||||
authentication.
|
||||
"""
|
||||
def __init__(self):
|
||||
Test.__init__(self)
|
||||
self.request = None
|
||||
|
||||
def render(self, request):
|
||||
self.request = request
|
||||
return Test.render(self, request)
|
||||
|
||||
def xmlrpc_authinfo(self):
|
||||
return self.request.getUser(), self.request.getPassword()
|
||||
|
||||
|
||||
|
||||
class TestQueryProtocol(xmlrpc.QueryProtocol):
|
||||
"""
|
||||
QueryProtocol for tests that saves headers received and sent,
|
||||
inside the factory.
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
self.factory.transport = self.transport
|
||||
xmlrpc.QueryProtocol.connectionMade(self)
|
||||
|
||||
def handleHeader(self, key, val):
|
||||
self.factory.headers[key.lower()] = val
|
||||
|
||||
def sendHeader(self, key, val):
|
||||
"""
|
||||
Keep sent headers so we can inspect them later.
|
||||
"""
|
||||
self.factory.sent_headers[key.lower()] = val
|
||||
xmlrpc.QueryProtocol.sendHeader(self, key, val)
|
||||
|
||||
|
||||
|
||||
class TestQueryFactory(xmlrpc._QueryFactory):
|
||||
"""
|
||||
QueryFactory using L{TestQueryProtocol} for saving headers.
|
||||
"""
|
||||
protocol = TestQueryProtocol
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.headers = {}
|
||||
self.sent_headers = {}
|
||||
xmlrpc._QueryFactory.__init__(self, *args, **kwargs)
|
||||
|
||||
|
||||
class TestQueryFactoryCancel(xmlrpc._QueryFactory):
|
||||
"""
|
||||
QueryFactory that saves a reference to the
|
||||
L{twisted.internet.interfaces.IConnector} to test connection lost.
|
||||
"""
|
||||
|
||||
def startedConnecting(self, connector):
|
||||
self.connector = connector
|
||||
|
||||
|
||||
class XMLRPCTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.p = reactor.listenTCP(0, server.Site(Test()),
|
||||
interface="127.0.0.1")
|
||||
self.port = self.p.getHost().port
|
||||
self.factories = []
|
||||
|
||||
def tearDown(self):
|
||||
self.factories = []
|
||||
return self.p.stopListening()
|
||||
|
||||
def queryFactory(self, *args, **kwargs):
|
||||
"""
|
||||
Specific queryFactory for proxy that uses our custom
|
||||
L{TestQueryFactory}, and save factories.
|
||||
"""
|
||||
factory = TestQueryFactory(*args, **kwargs)
|
||||
self.factories.append(factory)
|
||||
return factory
|
||||
|
||||
def proxy(self, factory=None):
|
||||
"""
|
||||
Return a new xmlrpc.Proxy for the test site created in
|
||||
setUp(), using the given factory as the queryFactory, or
|
||||
self.queryFactory if no factory is provided.
|
||||
"""
|
||||
p = xmlrpc.Proxy(networkString("http://127.0.0.1:%d/" % self.port))
|
||||
if factory is None:
|
||||
p.queryFactory = self.queryFactory
|
||||
else:
|
||||
p.queryFactory = factory
|
||||
return p
|
||||
|
||||
def test_results(self):
|
||||
inputOutput = [
|
||||
("add", (2, 3), 5),
|
||||
("defer", ("a",), "a"),
|
||||
("dict", ({"a": 1}, "a"), 1),
|
||||
("pair", ("a", 1), ["a", 1]),
|
||||
("snowman", (u"\u2603"), u"\u2603"),
|
||||
("complex", (), {"a": ["b", "c", 12, []], "D": "foo"})]
|
||||
|
||||
dl = []
|
||||
for meth, args, outp in inputOutput:
|
||||
d = self.proxy().callRemote(meth, *args)
|
||||
d.addCallback(self.assertEqual, outp)
|
||||
dl.append(d)
|
||||
return defer.DeferredList(dl, fireOnOneErrback=True)
|
||||
|
||||
|
||||
def test_headers(self):
|
||||
"""
|
||||
Verify that headers sent from the client side and the ones we
|
||||
get back from the server side are correct.
|
||||
|
||||
"""
|
||||
d = self.proxy().callRemote("snowman", u"\u2603")
|
||||
|
||||
def check_server_headers(ing):
|
||||
self.assertEqual(
|
||||
self.factories[0].headers[b'content-type'],
|
||||
b'text/xml; charset=utf-8')
|
||||
self.assertEqual(
|
||||
self.factories[0].headers[b'content-length'], b'129')
|
||||
|
||||
def check_client_headers(ign):
|
||||
self.assertEqual(
|
||||
self.factories[0].sent_headers[b'user-agent'],
|
||||
b'Twisted/XMLRPClib')
|
||||
self.assertEqual(
|
||||
self.factories[0].sent_headers[b'content-type'],
|
||||
b'text/xml; charset=utf-8')
|
||||
self.assertEqual(
|
||||
self.factories[0].sent_headers[b'content-length'], b'155')
|
||||
|
||||
d.addCallback(check_server_headers)
|
||||
d.addCallback(check_client_headers)
|
||||
return d
|
||||
|
||||
|
||||
def test_errors(self):
|
||||
"""
|
||||
Verify that for each way a method exposed via XML-RPC can fail, the
|
||||
correct 'Content-type' header is set in the response and that the
|
||||
client-side Deferred is errbacked with an appropriate C{Fault}
|
||||
instance.
|
||||
"""
|
||||
logObserver = EventLoggingObserver()
|
||||
filtered = FilteringLogObserver(
|
||||
logObserver,
|
||||
[LogLevelFilterPredicate(defaultLogLevel=LogLevel.critical)]
|
||||
)
|
||||
globalLogPublisher.addObserver(filtered)
|
||||
self.addCleanup(lambda: globalLogPublisher.removeObserver(filtered))
|
||||
dl = []
|
||||
for code, methodName in [(666, "fail"), (666, "deferFail"),
|
||||
(12, "fault"), (23, "noSuchMethod"),
|
||||
(17, "deferFault"), (42, "SESSION_TEST")]:
|
||||
d = self.proxy().callRemote(methodName)
|
||||
d = self.assertFailure(d, xmlrpc.Fault)
|
||||
d.addCallback(lambda exc, code=code:
|
||||
self.assertEqual(exc.faultCode, code))
|
||||
dl.append(d)
|
||||
d = defer.DeferredList(dl, fireOnOneErrback=True)
|
||||
def cb(ign):
|
||||
for factory in self.factories:
|
||||
self.assertEqual(factory.headers[b'content-type'],
|
||||
b'text/xml; charset=utf-8')
|
||||
self.assertEquals(2, len(logObserver))
|
||||
f1 = logObserver[0]["log_failure"].value
|
||||
f2 = logObserver[1]["log_failure"].value
|
||||
|
||||
if isinstance(f1, TestValueError):
|
||||
self.assertIsInstance(f2, TestRuntimeError)
|
||||
else:
|
||||
self.assertIsInstance(f1, TestRuntimeError)
|
||||
self.assertIsInstance(f2, TestValueError)
|
||||
|
||||
self.flushLoggedErrors(TestRuntimeError, TestValueError)
|
||||
d.addCallback(cb)
|
||||
return d
|
||||
|
||||
|
||||
def test_cancel(self):
|
||||
"""
|
||||
A deferred from the Proxy can be cancelled, disconnecting
|
||||
the L{twisted.internet.interfaces.IConnector}.
|
||||
"""
|
||||
def factory(*args, **kw):
|
||||
factory.f = TestQueryFactoryCancel(*args, **kw)
|
||||
return factory.f
|
||||
d = self.proxy(factory).callRemote('add', 2, 3)
|
||||
self.assertNotEqual(factory.f.connector.state, "disconnected")
|
||||
d.cancel()
|
||||
self.assertEqual(factory.f.connector.state, "disconnected")
|
||||
d = self.assertFailure(d, defer.CancelledError)
|
||||
return d
|
||||
|
||||
|
||||
def test_errorGet(self):
|
||||
"""
|
||||
A classic GET on the xml server should return a NOT_ALLOWED.
|
||||
"""
|
||||
agent = client.Agent(reactor)
|
||||
d = agent.request(b"GET", networkString("http://127.0.0.1:%d/" % (self.port,)))
|
||||
def checkResponse(response):
|
||||
self.assertEqual(response.code, http.NOT_ALLOWED)
|
||||
d.addCallback(checkResponse)
|
||||
return d
|
||||
|
||||
def test_errorXMLContent(self):
|
||||
"""
|
||||
Test that an invalid XML input returns an L{xmlrpc.Fault}.
|
||||
"""
|
||||
agent = client.Agent(reactor)
|
||||
d = agent.request(
|
||||
uri=networkString("http://127.0.0.1:%d/" % (self.port,)),
|
||||
method=b"POST",
|
||||
bodyProducer=client.FileBodyProducer(BytesIO(b"foo")))
|
||||
d.addCallback(client.readBody)
|
||||
def cb(result):
|
||||
self.assertRaises(xmlrpc.Fault, xmlrpclib.loads, result)
|
||||
d.addCallback(cb)
|
||||
return d
|
||||
|
||||
|
||||
def test_datetimeRoundtrip(self):
|
||||
|
||||
"""
|
||||
If an L{xmlrpclib.DateTime} is passed as an argument to an XML-RPC
|
||||
call and then returned by the server unmodified, the result should
|
||||
be equal to the original object.
|
||||
"""
|
||||
when = xmlrpclib.DateTime()
|
||||
d = self.proxy().callRemote("echo", when)
|
||||
d.addCallback(self.assertEqual, when)
|
||||
return d
|
||||
|
||||
|
||||
def test_doubleEncodingError(self):
|
||||
"""
|
||||
If it is not possible to encode a response to the request (for example,
|
||||
because L{xmlrpclib.dumps} raises an exception when encoding a
|
||||
L{Fault}) the exception which prevents the response from being
|
||||
generated is logged and the request object is finished anyway.
|
||||
"""
|
||||
logObserver = EventLoggingObserver()
|
||||
filtered = FilteringLogObserver(
|
||||
logObserver,
|
||||
[LogLevelFilterPredicate(defaultLogLevel=LogLevel.critical)]
|
||||
)
|
||||
globalLogPublisher.addObserver(filtered)
|
||||
self.addCleanup(lambda: globalLogPublisher.removeObserver(filtered))
|
||||
d = self.proxy().callRemote("echo", "")
|
||||
|
||||
# *Now* break xmlrpclib.dumps. Hopefully the client already used it.
|
||||
def fakeDumps(*args, **kwargs):
|
||||
raise RuntimeError("Cannot encode anything at all!")
|
||||
self.patch(xmlrpclib, 'dumps', fakeDumps)
|
||||
|
||||
# It doesn't matter how it fails, so long as it does. Also, it happens
|
||||
# to fail with an implementation detail exception right now, not
|
||||
# something suitable as part of a public interface.
|
||||
d = self.assertFailure(d, Exception)
|
||||
|
||||
def cbFailed(ignored):
|
||||
# The fakeDumps exception should have been logged.
|
||||
self.assertEquals(1, len(logObserver))
|
||||
self.assertIsInstance(
|
||||
logObserver[0]["log_failure"].value,
|
||||
RuntimeError
|
||||
)
|
||||
self.assertEqual(len(self.flushLoggedErrors(RuntimeError)), 1)
|
||||
d.addCallback(cbFailed)
|
||||
return d
|
||||
|
||||
|
||||
def test_closeConnectionAfterRequest(self):
|
||||
"""
|
||||
The connection to the web server is closed when the request is done.
|
||||
"""
|
||||
d = self.proxy().callRemote('echo', '')
|
||||
def responseDone(ignored):
|
||||
[factory] = self.factories
|
||||
self.assertFalse(factory.transport.connected)
|
||||
self.assertTrue(factory.transport.disconnected)
|
||||
return d.addCallback(responseDone)
|
||||
|
||||
|
||||
def test_tcpTimeout(self):
|
||||
"""
|
||||
For I{HTTP} URIs, L{xmlrpc.Proxy.callRemote} passes the value it
|
||||
received for the C{connectTimeout} parameter as the C{timeout} argument
|
||||
to the underlying connectTCP call.
|
||||
"""
|
||||
reactor = MemoryReactor()
|
||||
proxy = xmlrpc.Proxy(b"http://127.0.0.1:69", connectTimeout=2.0,
|
||||
reactor=reactor)
|
||||
proxy.callRemote("someMethod")
|
||||
self.assertEqual(reactor.tcpClients[0][3], 2.0)
|
||||
|
||||
|
||||
def test_sslTimeout(self):
|
||||
"""
|
||||
For I{HTTPS} URIs, L{xmlrpc.Proxy.callRemote} passes the value it
|
||||
received for the C{connectTimeout} parameter as the C{timeout} argument
|
||||
to the underlying connectSSL call.
|
||||
"""
|
||||
reactor = MemoryReactor()
|
||||
proxy = xmlrpc.Proxy(b"https://127.0.0.1:69", connectTimeout=3.0,
|
||||
reactor=reactor)
|
||||
proxy.callRemote("someMethod")
|
||||
self.assertEqual(reactor.sslClients[0][4], 3.0)
|
||||
test_sslTimeout.skip = sslSkip
|
||||
|
||||
|
||||
|
||||
class XMLRPCProxyWithoutSlashTests(XMLRPCTests):
|
||||
"""
|
||||
Test with proxy that doesn't add a slash.
|
||||
"""
|
||||
|
||||
def proxy(self, factory=None):
|
||||
p = xmlrpc.Proxy(networkString("http://127.0.0.1:%d" % self.port))
|
||||
if factory is None:
|
||||
p.queryFactory = self.queryFactory
|
||||
else:
|
||||
p.queryFactory = factory
|
||||
return p
|
||||
|
||||
|
||||
|
||||
class XMLRPCPublicLookupProcedureTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for L{XMLRPC}'s support of subclasses which override
|
||||
C{lookupProcedure} and C{listProcedures}.
|
||||
"""
|
||||
|
||||
def createServer(self, resource):
|
||||
self.p = reactor.listenTCP(
|
||||
0, server.Site(resource), interface="127.0.0.1")
|
||||
self.addCleanup(self.p.stopListening)
|
||||
self.port = self.p.getHost().port
|
||||
self.proxy = xmlrpc.Proxy(
|
||||
networkString('http://127.0.0.1:%d' % self.port))
|
||||
|
||||
|
||||
def test_lookupProcedure(self):
|
||||
"""
|
||||
A subclass of L{XMLRPC} can override C{lookupProcedure} to find
|
||||
procedures that are not defined using a C{xmlrpc_}-prefixed method name.
|
||||
"""
|
||||
self.createServer(TestLookupProcedure())
|
||||
what = "hello"
|
||||
d = self.proxy.callRemote("echo", what)
|
||||
d.addCallback(self.assertEqual, what)
|
||||
return d
|
||||
|
||||
|
||||
def test_errors(self):
|
||||
"""
|
||||
A subclass of L{XMLRPC} can override C{lookupProcedure} to raise
|
||||
L{NoSuchFunction} to indicate that a requested method is not available
|
||||
to be called, signalling a fault to the XML-RPC client.
|
||||
"""
|
||||
self.createServer(TestLookupProcedure())
|
||||
d = self.proxy.callRemote("xxxx", "hello")
|
||||
d = self.assertFailure(d, xmlrpc.Fault)
|
||||
return d
|
||||
|
||||
|
||||
def test_listMethods(self):
|
||||
"""
|
||||
A subclass of L{XMLRPC} can override C{listProcedures} to define
|
||||
Overriding listProcedures should prevent introspection from being
|
||||
broken.
|
||||
"""
|
||||
resource = TestListProcedures()
|
||||
addIntrospection(resource)
|
||||
self.createServer(resource)
|
||||
d = self.proxy.callRemote("system.listMethods")
|
||||
def listed(procedures):
|
||||
# The list will also include other introspection procedures added by
|
||||
# addIntrospection. We just want to see "foo" from our customized
|
||||
# listProcedures.
|
||||
self.assertIn('foo', procedures)
|
||||
d.addCallback(listed)
|
||||
return d
|
||||
|
||||
|
||||
|
||||
class SerializationConfigMixin:
|
||||
"""
|
||||
Mixin which defines a couple tests which should pass when a particular flag
|
||||
is passed to L{XMLRPC}.
|
||||
|
||||
These are not meant to be exhaustive serialization tests, since L{xmlrpclib}
|
||||
does all of the actual serialization work. They are just meant to exercise
|
||||
a few codepaths to make sure we are calling into xmlrpclib correctly.
|
||||
|
||||
@ivar flagName: A C{str} giving the name of the flag which must be passed to
|
||||
L{XMLRPC} to allow the tests to pass. Subclasses should set this.
|
||||
|
||||
@ivar value: A value which the specified flag will allow the serialization
|
||||
of. Subclasses should set this.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Create a new XML-RPC server with C{allowNone} set to C{True}.
|
||||
"""
|
||||
kwargs = {self.flagName: True}
|
||||
self.p = reactor.listenTCP(
|
||||
0, server.Site(Test(**kwargs)), interface="127.0.0.1")
|
||||
self.addCleanup(self.p.stopListening)
|
||||
self.port = self.p.getHost().port
|
||||
self.proxy = xmlrpc.Proxy(
|
||||
networkString("http://127.0.0.1:%d/" % (self.port,)), **kwargs)
|
||||
|
||||
|
||||
def test_roundtripValue(self):
|
||||
"""
|
||||
C{self.value} can be round-tripped over an XMLRPC method call/response.
|
||||
"""
|
||||
d = self.proxy.callRemote('defer', self.value)
|
||||
d.addCallback(self.assertEqual, self.value)
|
||||
return d
|
||||
|
||||
|
||||
def test_roundtripNestedValue(self):
|
||||
"""
|
||||
A C{dict} which contains C{self.value} can be round-tripped over an
|
||||
XMLRPC method call/response.
|
||||
"""
|
||||
d = self.proxy.callRemote('defer', {'a': self.value})
|
||||
d.addCallback(self.assertEqual, {'a': self.value})
|
||||
return d
|
||||
|
||||
|
||||
|
||||
class XMLRPCAllowNoneTests(SerializationConfigMixin, unittest.TestCase):
|
||||
"""
|
||||
Tests for passing L{None} when the C{allowNone} flag is set.
|
||||
"""
|
||||
flagName = "allowNone"
|
||||
value = None
|
||||
|
||||
|
||||
class XMLRPCUseDateTimeTests(SerializationConfigMixin, unittest.TestCase):
|
||||
"""
|
||||
Tests for passing a C{datetime.datetime} instance when the C{useDateTime}
|
||||
flag is set.
|
||||
"""
|
||||
flagName = "useDateTime"
|
||||
value = datetime.datetime(2000, 12, 28, 3, 45, 59)
|
||||
|
||||
|
||||
class XMLRPCAuthenticatedTests(XMLRPCTests):
|
||||
"""
|
||||
Test with authenticated proxy. We run this with the same input/output as
|
||||
above.
|
||||
"""
|
||||
user = b"username"
|
||||
password = b"asecret"
|
||||
|
||||
def setUp(self):
|
||||
self.p = reactor.listenTCP(0, server.Site(TestAuthHeader()),
|
||||
interface="127.0.0.1")
|
||||
self.port = self.p.getHost().port
|
||||
self.factories = []
|
||||
|
||||
|
||||
def test_authInfoInURL(self):
|
||||
url = "http://%s:%s@127.0.0.1:%d/" % (
|
||||
nativeString(self.user), nativeString(self.password), self.port)
|
||||
p = xmlrpc.Proxy(networkString(url))
|
||||
d = p.callRemote("authinfo")
|
||||
d.addCallback(self.assertEqual, [self.user, self.password])
|
||||
return d
|
||||
|
||||
|
||||
def test_explicitAuthInfo(self):
|
||||
p = xmlrpc.Proxy(networkString("http://127.0.0.1:%d/" % (
|
||||
self.port,)), self.user, self.password)
|
||||
d = p.callRemote("authinfo")
|
||||
d.addCallback(self.assertEqual, [self.user, self.password])
|
||||
return d
|
||||
|
||||
|
||||
def test_longPassword(self):
|
||||
"""
|
||||
C{QueryProtocol} uses the C{base64.b64encode} function to encode user
|
||||
name and password in the I{Authorization} header, so that it doesn't
|
||||
embed new lines when using long inputs.
|
||||
"""
|
||||
longPassword = self.password * 40
|
||||
p = xmlrpc.Proxy(networkString("http://127.0.0.1:%d/" % (
|
||||
self.port,)), self.user, longPassword)
|
||||
d = p.callRemote("authinfo")
|
||||
d.addCallback(self.assertEqual, [self.user, longPassword])
|
||||
return d
|
||||
|
||||
|
||||
def test_explicitAuthInfoOverride(self):
|
||||
p = xmlrpc.Proxy(networkString("http://wrong:info@127.0.0.1:%d/" % (
|
||||
self.port,)), self.user, self.password)
|
||||
d = p.callRemote("authinfo")
|
||||
d.addCallback(self.assertEqual, [self.user, self.password])
|
||||
return d
|
||||
|
||||
|
||||
class XMLRPCIntrospectionTests(XMLRPCTests):
|
||||
|
||||
def setUp(self):
|
||||
xmlrpc = Test()
|
||||
addIntrospection(xmlrpc)
|
||||
self.p = reactor.listenTCP(0, server.Site(xmlrpc),interface="127.0.0.1")
|
||||
self.port = self.p.getHost().port
|
||||
self.factories = []
|
||||
|
||||
def test_listMethods(self):
|
||||
|
||||
def cbMethods(meths):
|
||||
meths.sort()
|
||||
self.assertEqual(
|
||||
meths,
|
||||
['add', 'complex', 'defer', 'deferFail',
|
||||
'deferFault', 'dict', 'echo', 'fail', 'fault',
|
||||
'pair', 'snowman', 'system.listMethods',
|
||||
'system.methodHelp',
|
||||
'system.methodSignature', 'withRequest'])
|
||||
|
||||
d = self.proxy().callRemote("system.listMethods")
|
||||
d.addCallback(cbMethods)
|
||||
return d
|
||||
|
||||
def test_methodHelp(self):
|
||||
inputOutputs = [
|
||||
("defer", "Help for defer."),
|
||||
("fail", ""),
|
||||
("dict", "Help for dict.")]
|
||||
|
||||
dl = []
|
||||
for meth, expected in inputOutputs:
|
||||
d = self.proxy().callRemote("system.methodHelp", meth)
|
||||
d.addCallback(self.assertEqual, expected)
|
||||
dl.append(d)
|
||||
return defer.DeferredList(dl, fireOnOneErrback=True)
|
||||
|
||||
def test_methodSignature(self):
|
||||
inputOutputs = [
|
||||
("defer", ""),
|
||||
("add", [['int', 'int', 'int'],
|
||||
['double', 'double', 'double']]),
|
||||
("pair", [['array', 'string', 'int']])]
|
||||
|
||||
dl = []
|
||||
for meth, expected in inputOutputs:
|
||||
d = self.proxy().callRemote("system.methodSignature", meth)
|
||||
d.addCallback(self.assertEqual, expected)
|
||||
dl.append(d)
|
||||
return defer.DeferredList(dl, fireOnOneErrback=True)
|
||||
|
||||
|
||||
class XMLRPCClientErrorHandlingTests(unittest.TestCase):
|
||||
"""
|
||||
Test error handling on the xmlrpc client.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.resource = static.Data(
|
||||
b"This text is not a valid XML-RPC response.",
|
||||
b"text/plain")
|
||||
self.resource.isLeaf = True
|
||||
|
||||
self.port = reactor.listenTCP(0, server.Site(self.resource),
|
||||
interface='127.0.0.1')
|
||||
|
||||
def tearDown(self):
|
||||
return self.port.stopListening()
|
||||
|
||||
def test_erroneousResponse(self):
|
||||
"""
|
||||
Test that calling the xmlrpc client on a static http server raises
|
||||
an exception.
|
||||
"""
|
||||
proxy = xmlrpc.Proxy(networkString("http://127.0.0.1:%d/" %
|
||||
(self.port.getHost().port,)))
|
||||
return self.assertFailure(proxy.callRemote("someMethod"), ValueError)
|
||||
|
||||
|
||||
|
||||
class QueryFactoryParseResponseTests(unittest.TestCase):
|
||||
"""
|
||||
Test the behaviour of L{_QueryFactory.parseResponse}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# The _QueryFactory that we are testing. We don't care about any
|
||||
# of the constructor parameters.
|
||||
self.queryFactory = _QueryFactory(
|
||||
path=None, host=None, method='POST', user=None, password=None,
|
||||
allowNone=False, args=())
|
||||
# An XML-RPC response that will parse without raising an error.
|
||||
self.goodContents = xmlrpclib.dumps(('',))
|
||||
# An 'XML-RPC response' that will raise a parsing error.
|
||||
self.badContents = 'invalid xml'
|
||||
# A dummy 'reason' to pass to clientConnectionLost. We don't care
|
||||
# what it is.
|
||||
self.reason = failure.Failure(ConnectionDone())
|
||||
|
||||
|
||||
def test_parseResponseCallbackSafety(self):
|
||||
"""
|
||||
We can safely call L{_QueryFactory.clientConnectionLost} as a callback
|
||||
of L{_QueryFactory.parseResponse}.
|
||||
"""
|
||||
d = self.queryFactory.deferred
|
||||
# The failure mode is that this callback raises an AlreadyCalled
|
||||
# error. We have to add it now so that it gets called synchronously
|
||||
# and triggers the race condition.
|
||||
d.addCallback(self.queryFactory.clientConnectionLost, self.reason)
|
||||
self.queryFactory.parseResponse(self.goodContents)
|
||||
return d
|
||||
|
||||
|
||||
def test_parseResponseErrbackSafety(self):
|
||||
"""
|
||||
We can safely call L{_QueryFactory.clientConnectionLost} as an errback
|
||||
of L{_QueryFactory.parseResponse}.
|
||||
"""
|
||||
d = self.queryFactory.deferred
|
||||
# The failure mode is that this callback raises an AlreadyCalled
|
||||
# error. We have to add it now so that it gets called synchronously
|
||||
# and triggers the race condition.
|
||||
d.addErrback(self.queryFactory.clientConnectionLost, self.reason)
|
||||
self.queryFactory.parseResponse(self.badContents)
|
||||
return d
|
||||
|
||||
|
||||
def test_badStatusErrbackSafety(self):
|
||||
"""
|
||||
We can safely call L{_QueryFactory.clientConnectionLost} as an errback
|
||||
of L{_QueryFactory.badStatus}.
|
||||
"""
|
||||
d = self.queryFactory.deferred
|
||||
# The failure mode is that this callback raises an AlreadyCalled
|
||||
# error. We have to add it now so that it gets called synchronously
|
||||
# and triggers the race condition.
|
||||
d.addErrback(self.queryFactory.clientConnectionLost, self.reason)
|
||||
self.queryFactory.badStatus('status', 'message')
|
||||
return d
|
||||
|
||||
def test_parseResponseWithoutData(self):
|
||||
"""
|
||||
Some server can send a response without any data:
|
||||
L{_QueryFactory.parseResponse} should catch the error and call the
|
||||
result errback.
|
||||
"""
|
||||
content = """
|
||||
<methodResponse>
|
||||
<params>
|
||||
<param>
|
||||
</param>
|
||||
</params>
|
||||
</methodResponse>"""
|
||||
d = self.queryFactory.deferred
|
||||
self.queryFactory.parseResponse(content)
|
||||
return self.assertFailure(d, IndexError)
|
||||
|
||||
|
||||
|
||||
class XMLRPCWithRequestTests(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.resource = Test()
|
||||
|
||||
|
||||
def test_withRequest(self):
|
||||
"""
|
||||
When an XML-RPC method is called and the implementation is
|
||||
decorated with L{withRequest}, the request object is passed as
|
||||
the first argument.
|
||||
"""
|
||||
request = DummyRequest('/RPC2')
|
||||
request.method = "POST"
|
||||
request.content = NativeStringIO(xmlrpclib.dumps(
|
||||
("foo",), 'withRequest'))
|
||||
def valid(n, request):
|
||||
data = xmlrpclib.loads(request.written[0])
|
||||
self.assertEqual(data, (('POST foo',), None))
|
||||
d = request.notifyFinish().addCallback(valid, request)
|
||||
self.resource.render_POST(request)
|
||||
return d
|
||||
@@ -0,0 +1,321 @@
|
||||
# -*- test-case-name: twisted.web.test.test_cgi -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
I hold resource classes and helper classes that deal with CGI scripts.
|
||||
"""
|
||||
|
||||
# System Imports
|
||||
import os
|
||||
import urllib
|
||||
|
||||
# Twisted Imports
|
||||
from twisted.internet import protocol
|
||||
from twisted.logger import Logger
|
||||
from twisted.python import filepath
|
||||
from twisted.spread import pb
|
||||
from twisted.web import http, resource, server, static
|
||||
|
||||
|
||||
class CGIDirectory(resource.Resource, filepath.FilePath):
|
||||
def __init__(self, pathname):
|
||||
resource.Resource.__init__(self)
|
||||
filepath.FilePath.__init__(self, pathname)
|
||||
|
||||
|
||||
def getChild(self, path, request):
|
||||
fnp = self.child(path)
|
||||
if not fnp.exists():
|
||||
return static.File.childNotFound
|
||||
elif fnp.isdir():
|
||||
return CGIDirectory(fnp.path)
|
||||
else:
|
||||
return CGIScript(fnp.path)
|
||||
return resource.NoResource()
|
||||
|
||||
|
||||
def render(self, request):
|
||||
notFound = resource.NoResource(
|
||||
"CGI directories do not support directory listing.")
|
||||
return notFound.render(request)
|
||||
|
||||
|
||||
|
||||
class CGIScript(resource.Resource):
|
||||
"""
|
||||
L{CGIScript} is a resource which runs child processes according to the CGI
|
||||
specification.
|
||||
|
||||
The implementation is complex due to the fact that it requires asynchronous
|
||||
IPC with an external process with an unpleasant protocol.
|
||||
"""
|
||||
isLeaf = 1
|
||||
def __init__(self, filename, registry=None, reactor=None):
|
||||
"""
|
||||
Initialize, with the name of a CGI script file.
|
||||
"""
|
||||
self.filename = filename
|
||||
if reactor is None:
|
||||
# This installs a default reactor, if None was installed before.
|
||||
# We do a late import here, so that importing the current module
|
||||
# won't directly trigger installing a default reactor.
|
||||
from twisted.internet import reactor
|
||||
self._reactor = reactor
|
||||
|
||||
|
||||
def render(self, request):
|
||||
"""
|
||||
Do various things to conform to the CGI specification.
|
||||
|
||||
I will set up the usual slew of environment variables, then spin off a
|
||||
process.
|
||||
|
||||
@type request: L{twisted.web.http.Request}
|
||||
@param request: An HTTP request.
|
||||
"""
|
||||
scriptName = b"/" + b"/".join(request.prepath)
|
||||
serverName = request.getRequestHostname().split(b':')[0]
|
||||
env = {"SERVER_SOFTWARE": server.version,
|
||||
"SERVER_NAME": serverName,
|
||||
"GATEWAY_INTERFACE": "CGI/1.1",
|
||||
"SERVER_PROTOCOL": request.clientproto,
|
||||
"SERVER_PORT": str(request.getHost().port),
|
||||
"REQUEST_METHOD": request.method,
|
||||
"SCRIPT_NAME": scriptName,
|
||||
"SCRIPT_FILENAME": self.filename,
|
||||
"REQUEST_URI": request.uri}
|
||||
|
||||
ip = request.getClientAddress().host
|
||||
if ip is not None:
|
||||
env['REMOTE_ADDR'] = ip
|
||||
pp = request.postpath
|
||||
if pp:
|
||||
env["PATH_INFO"] = "/" + "/".join(pp)
|
||||
|
||||
if hasattr(request, "content"):
|
||||
# 'request.content' is either a StringIO or a TemporaryFile, and
|
||||
# the file pointer is sitting at the beginning (seek(0,0))
|
||||
request.content.seek(0, 2)
|
||||
length = request.content.tell()
|
||||
request.content.seek(0, 0)
|
||||
env['CONTENT_LENGTH'] = str(length)
|
||||
|
||||
try:
|
||||
qindex = request.uri.index(b'?')
|
||||
except ValueError:
|
||||
env['QUERY_STRING'] = ''
|
||||
qargs = []
|
||||
else:
|
||||
qs = env['QUERY_STRING'] = request.uri[qindex+1:]
|
||||
if '=' in qs:
|
||||
qargs = []
|
||||
else:
|
||||
qargs = [urllib.unquote(x) for x in qs.split('+')]
|
||||
|
||||
# Propagate HTTP headers
|
||||
for title, header in request.getAllHeaders().items():
|
||||
envname = title.replace(b'-', b'_').upper()
|
||||
if title not in (b'content-type', b'content-length', b'proxy'):
|
||||
envname = b"HTTP_" + envname
|
||||
env[envname] = header
|
||||
# Propagate our environment
|
||||
for key, value in os.environ.items():
|
||||
if key not in env:
|
||||
env[key] = value
|
||||
# And they're off!
|
||||
self.runProcess(env, request, qargs)
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
|
||||
def runProcess(self, env, request, qargs=[]):
|
||||
"""
|
||||
Run the cgi script.
|
||||
|
||||
@type env: A L{dict} of L{str}, or L{None}
|
||||
@param env: The environment variables to pass to the process that will
|
||||
get spawned. See
|
||||
L{twisted.internet.interfaces.IReactorProcess.spawnProcess} for
|
||||
more information about environments and process creation.
|
||||
|
||||
@type request: L{twisted.web.http.Request}
|
||||
@param request: An HTTP request.
|
||||
|
||||
@type qargs: A L{list} of L{str}
|
||||
@param qargs: The command line arguments to pass to the process that
|
||||
will get spawned.
|
||||
"""
|
||||
p = CGIProcessProtocol(request)
|
||||
self._reactor.spawnProcess(p, self.filename, [self.filename] + qargs,
|
||||
env, os.path.dirname(self.filename))
|
||||
|
||||
|
||||
|
||||
class FilteredScript(CGIScript):
|
||||
"""
|
||||
I am a special version of a CGI script, that uses a specific executable.
|
||||
|
||||
This is useful for interfacing with other scripting languages that adhere
|
||||
to the CGI standard. My C{filter} attribute specifies what executable to
|
||||
run, and my C{filename} init parameter describes which script to pass to
|
||||
the first argument of that script.
|
||||
|
||||
To customize me for a particular location of a CGI interpreter, override
|
||||
C{filter}.
|
||||
|
||||
@type filter: L{str}
|
||||
@ivar filter: The absolute path to the executable.
|
||||
"""
|
||||
|
||||
filter = '/usr/bin/cat'
|
||||
|
||||
|
||||
def runProcess(self, env, request, qargs=[]):
|
||||
"""
|
||||
Run a script through the C{filter} executable.
|
||||
|
||||
@type env: A L{dict} of L{str}, or L{None}
|
||||
@param env: The environment variables to pass to the process that will
|
||||
get spawned. See
|
||||
L{twisted.internet.interfaces.IReactorProcess.spawnProcess}
|
||||
for more information about environments and process creation.
|
||||
|
||||
@type request: L{twisted.web.http.Request}
|
||||
@param request: An HTTP request.
|
||||
|
||||
@type qargs: A L{list} of L{str}
|
||||
@param qargs: The command line arguments to pass to the process that
|
||||
will get spawned.
|
||||
"""
|
||||
p = CGIProcessProtocol(request)
|
||||
self._reactor.spawnProcess(p, self.filter,
|
||||
[self.filter, self.filename] + qargs, env,
|
||||
os.path.dirname(self.filename))
|
||||
|
||||
|
||||
|
||||
class CGIProcessProtocol(protocol.ProcessProtocol, pb.Viewable):
|
||||
handling_headers = 1
|
||||
headers_written = 0
|
||||
headertext = b''
|
||||
errortext = b''
|
||||
_log = Logger()
|
||||
|
||||
# Remotely relay producer interface.
|
||||
|
||||
def view_resumeProducing(self, issuer):
|
||||
self.resumeProducing()
|
||||
|
||||
|
||||
def view_pauseProducing(self, issuer):
|
||||
self.pauseProducing()
|
||||
|
||||
|
||||
def view_stopProducing(self, issuer):
|
||||
self.stopProducing()
|
||||
|
||||
|
||||
def resumeProducing(self):
|
||||
self.transport.resumeProducing()
|
||||
|
||||
|
||||
def pauseProducing(self):
|
||||
self.transport.pauseProducing()
|
||||
|
||||
|
||||
def stopProducing(self):
|
||||
self.transport.loseConnection()
|
||||
|
||||
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
|
||||
def connectionMade(self):
|
||||
self.request.registerProducer(self, 1)
|
||||
self.request.content.seek(0, 0)
|
||||
content = self.request.content.read()
|
||||
if content:
|
||||
self.transport.write(content)
|
||||
self.transport.closeStdin()
|
||||
|
||||
|
||||
def errReceived(self, error):
|
||||
self.errortext = self.errortext + error
|
||||
|
||||
|
||||
def outReceived(self, output):
|
||||
"""
|
||||
Handle a chunk of input
|
||||
"""
|
||||
# First, make sure that the headers from the script are sorted
|
||||
# out (we'll want to do some parsing on these later.)
|
||||
if self.handling_headers:
|
||||
text = self.headertext + output
|
||||
headerEnds = []
|
||||
for delimiter in b'\n\n', b'\r\n\r\n', b'\r\r', b'\n\r\n':
|
||||
headerend = text.find(delimiter)
|
||||
if headerend != -1:
|
||||
headerEnds.append((headerend, delimiter))
|
||||
if headerEnds:
|
||||
# The script is entirely in control of response headers;
|
||||
# disable the default Content-Type value normally provided by
|
||||
# twisted.web.server.Request.
|
||||
self.request.defaultContentType = None
|
||||
|
||||
headerEnds.sort()
|
||||
headerend, delimiter = headerEnds[0]
|
||||
self.headertext = text[:headerend]
|
||||
# This is a final version of the header text.
|
||||
linebreak = delimiter[:len(delimiter)//2]
|
||||
headers = self.headertext.split(linebreak)
|
||||
for header in headers:
|
||||
br = header.find(b': ')
|
||||
if br == -1:
|
||||
self._log.error(
|
||||
'ignoring malformed CGI header: {header!r}',
|
||||
header=header)
|
||||
else:
|
||||
headerName = header[:br].lower()
|
||||
headerText = header[br+2:]
|
||||
if headerName == b'location':
|
||||
self.request.setResponseCode(http.FOUND)
|
||||
if headerName == b'status':
|
||||
try:
|
||||
# "XXX <description>" sometimes happens.
|
||||
statusNum = int(headerText[:3])
|
||||
except:
|
||||
self._log.error("malformed status header")
|
||||
else:
|
||||
self.request.setResponseCode(statusNum)
|
||||
else:
|
||||
# Don't allow the application to control
|
||||
# these required headers.
|
||||
if headerName.lower() not in (b'server', b'date'):
|
||||
self.request.responseHeaders.addRawHeader(
|
||||
headerName, headerText)
|
||||
output = text[headerend+len(delimiter):]
|
||||
self.handling_headers = 0
|
||||
if self.handling_headers:
|
||||
self.headertext = text
|
||||
if not self.handling_headers:
|
||||
self.request.write(output)
|
||||
|
||||
|
||||
def processEnded(self, reason):
|
||||
if reason.value.exitCode != 0:
|
||||
self._log.error("CGI {uri} exited with exit code {exitCode}",
|
||||
uri=self.request.uri, exitCode=reason.value.exitCode)
|
||||
if self.errortext:
|
||||
self._log.error("Errors from CGI {uri}: {errorText}",
|
||||
uri=self.request.uri, errorText=self.errortext)
|
||||
if self.handling_headers:
|
||||
self._log.error("Premature end of headers in {uri}: {headerText}",
|
||||
uri=self.request.uri, headerText=self.headertext)
|
||||
self.request.write(
|
||||
resource.ErrorPage(http.INTERNAL_SERVER_ERROR,
|
||||
"CGI Script Error",
|
||||
"Premature end of script headers.").render(self.request))
|
||||
self.request.unregisterProducer()
|
||||
self.request.finish()
|
||||
@@ -0,0 +1,591 @@
|
||||
# -*- test-case-name: twisted.web.test.test_xmlrpc -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
A generic resource for publishing objects via XML-RPC.
|
||||
|
||||
Maintainer: Itamar Shtull-Trauring
|
||||
|
||||
@var Fault: See L{xmlrpclib.Fault}
|
||||
@type Fault: L{xmlrpclib.Fault}
|
||||
"""
|
||||
|
||||
from __future__ import division, absolute_import
|
||||
|
||||
from twisted.python.compat import _PY3, intToBytes, nativeString, urllib_parse
|
||||
from twisted.python.compat import unicode
|
||||
|
||||
# System Imports
|
||||
import base64
|
||||
if _PY3:
|
||||
import xmlrpc.client as xmlrpclib
|
||||
else:
|
||||
import xmlrpclib
|
||||
|
||||
# Sibling Imports
|
||||
from twisted.web import resource, server, http
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.python import reflect, failure
|
||||
from twisted.logger import Logger
|
||||
|
||||
# These are deprecated, use the class level definitions
|
||||
NOT_FOUND = 8001
|
||||
FAILURE = 8002
|
||||
|
||||
|
||||
# Useful so people don't need to import xmlrpclib directly
|
||||
Fault = xmlrpclib.Fault
|
||||
Binary = xmlrpclib.Binary
|
||||
Boolean = xmlrpclib.Boolean
|
||||
DateTime = xmlrpclib.DateTime
|
||||
|
||||
|
||||
def withRequest(f):
|
||||
"""
|
||||
Decorator to cause the request to be passed as the first argument
|
||||
to the method.
|
||||
|
||||
If an I{xmlrpc_} method is wrapped with C{withRequest}, the
|
||||
request object is passed as the first argument to that method.
|
||||
For example::
|
||||
|
||||
@withRequest
|
||||
def xmlrpc_echo(self, request, s):
|
||||
return s
|
||||
|
||||
@since: 10.2
|
||||
"""
|
||||
f.withRequest = True
|
||||
return f
|
||||
|
||||
|
||||
|
||||
class NoSuchFunction(Fault):
|
||||
"""
|
||||
There is no function by the given name.
|
||||
"""
|
||||
|
||||
|
||||
class Handler:
|
||||
"""
|
||||
Handle a XML-RPC request and store the state for a request in progress.
|
||||
|
||||
Override the run() method and return result using self.result,
|
||||
a Deferred.
|
||||
|
||||
We require this class since we're not using threads, so we can't
|
||||
encapsulate state in a running function if we're going to have
|
||||
to wait for results.
|
||||
|
||||
For example, lets say we want to authenticate against twisted.cred,
|
||||
run a LDAP query and then pass its result to a database query, all
|
||||
as a result of a single XML-RPC command. We'd use a Handler instance
|
||||
to store the state of the running command.
|
||||
"""
|
||||
|
||||
def __init__(self, resource, *args):
|
||||
self.resource = resource # the XML-RPC resource we are connected to
|
||||
self.result = defer.Deferred()
|
||||
self.run(*args)
|
||||
|
||||
def run(self, *args):
|
||||
# event driven equivalent of 'raise UnimplementedError'
|
||||
self.result.errback(
|
||||
NotImplementedError("Implement run() in subclasses"))
|
||||
|
||||
|
||||
class XMLRPC(resource.Resource):
|
||||
"""
|
||||
A resource that implements XML-RPC.
|
||||
|
||||
You probably want to connect this to '/RPC2'.
|
||||
|
||||
Methods published can return XML-RPC serializable results, Faults,
|
||||
Binary, Boolean, DateTime, Deferreds, or Handler instances.
|
||||
|
||||
By default methods beginning with 'xmlrpc_' are published.
|
||||
|
||||
Sub-handlers for prefixed methods (e.g., system.listMethods)
|
||||
can be added with putSubHandler. By default, prefixes are
|
||||
separated with a '.'. Override self.separator to change this.
|
||||
|
||||
@ivar allowNone: Permit XML translating of Python constant None.
|
||||
@type allowNone: C{bool}
|
||||
|
||||
@ivar useDateTime: Present C{datetime} values as C{datetime.datetime}
|
||||
objects?
|
||||
@type useDateTime: C{bool}
|
||||
"""
|
||||
|
||||
# Error codes for Twisted, if they conflict with yours then
|
||||
# modify them at runtime.
|
||||
NOT_FOUND = 8001
|
||||
FAILURE = 8002
|
||||
|
||||
isLeaf = 1
|
||||
separator = '.'
|
||||
allowedMethods = (b'POST',)
|
||||
_log = Logger()
|
||||
|
||||
def __init__(self, allowNone=False, useDateTime=False):
|
||||
resource.Resource.__init__(self)
|
||||
self.subHandlers = {}
|
||||
self.allowNone = allowNone
|
||||
self.useDateTime = useDateTime
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self.__dict__[name] = value
|
||||
|
||||
|
||||
def putSubHandler(self, prefix, handler):
|
||||
self.subHandlers[prefix] = handler
|
||||
|
||||
def getSubHandler(self, prefix):
|
||||
return self.subHandlers.get(prefix, None)
|
||||
|
||||
def getSubHandlerPrefixes(self):
|
||||
return list(self.subHandlers.keys())
|
||||
|
||||
def render_POST(self, request):
|
||||
request.content.seek(0, 0)
|
||||
request.setHeader(b"content-type", b"text/xml; charset=utf-8")
|
||||
try:
|
||||
args, functionPath = xmlrpclib.loads(request.content.read(),
|
||||
use_datetime=self.useDateTime)
|
||||
except Exception as e:
|
||||
f = Fault(self.FAILURE, "Can't deserialize input: %s" % (e,))
|
||||
self._cbRender(f, request)
|
||||
else:
|
||||
try:
|
||||
function = self.lookupProcedure(functionPath)
|
||||
except Fault as f:
|
||||
self._cbRender(f, request)
|
||||
else:
|
||||
# Use this list to track whether the response has failed or not.
|
||||
# This will be used later on to decide if the result of the
|
||||
# Deferred should be written out and Request.finish called.
|
||||
responseFailed = []
|
||||
request.notifyFinish().addErrback(responseFailed.append)
|
||||
if getattr(function, 'withRequest', False):
|
||||
d = defer.maybeDeferred(function, request, *args)
|
||||
else:
|
||||
d = defer.maybeDeferred(function, *args)
|
||||
d.addErrback(self._ebRender)
|
||||
d.addCallback(self._cbRender, request, responseFailed)
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
|
||||
def _cbRender(self, result, request, responseFailed=None):
|
||||
if responseFailed:
|
||||
return
|
||||
|
||||
if isinstance(result, Handler):
|
||||
result = result.result
|
||||
if not isinstance(result, Fault):
|
||||
result = (result,)
|
||||
try:
|
||||
try:
|
||||
content = xmlrpclib.dumps(
|
||||
result, methodresponse=True,
|
||||
allow_none=self.allowNone)
|
||||
except Exception as e:
|
||||
f = Fault(self.FAILURE, "Can't serialize output: %s" % (e,))
|
||||
content = xmlrpclib.dumps(f, methodresponse=True,
|
||||
allow_none=self.allowNone)
|
||||
|
||||
if isinstance(content, unicode):
|
||||
content = content.encode('utf8')
|
||||
request.setHeader(
|
||||
b"content-length", intToBytes(len(content)))
|
||||
request.write(content)
|
||||
except:
|
||||
self._log.failure('')
|
||||
request.finish()
|
||||
|
||||
|
||||
def _ebRender(self, failure):
|
||||
if isinstance(failure.value, Fault):
|
||||
return failure.value
|
||||
self._log.failure('', failure)
|
||||
return Fault(self.FAILURE, "error")
|
||||
|
||||
|
||||
def lookupProcedure(self, procedurePath):
|
||||
"""
|
||||
Given a string naming a procedure, return a callable object for that
|
||||
procedure or raise NoSuchFunction.
|
||||
|
||||
The returned object will be called, and should return the result of the
|
||||
procedure, a Deferred, or a Fault instance.
|
||||
|
||||
Override in subclasses if you want your own policy. The base
|
||||
implementation that given C{'foo'}, C{self.xmlrpc_foo} will be returned.
|
||||
If C{procedurePath} contains C{self.separator}, the sub-handler for the
|
||||
initial prefix is used to search for the remaining path.
|
||||
|
||||
If you override C{lookupProcedure}, you may also want to override
|
||||
C{listProcedures} to accurately report the procedures supported by your
|
||||
resource, so that clients using the I{system.listMethods} procedure
|
||||
receive accurate results.
|
||||
|
||||
@since: 11.1
|
||||
"""
|
||||
if procedurePath.find(self.separator) != -1:
|
||||
prefix, procedurePath = procedurePath.split(self.separator, 1)
|
||||
handler = self.getSubHandler(prefix)
|
||||
if handler is None:
|
||||
raise NoSuchFunction(self.NOT_FOUND,
|
||||
"no such subHandler %s" % prefix)
|
||||
return handler.lookupProcedure(procedurePath)
|
||||
|
||||
f = getattr(self, "xmlrpc_%s" % procedurePath, None)
|
||||
if not f:
|
||||
raise NoSuchFunction(self.NOT_FOUND,
|
||||
"procedure %s not found" % procedurePath)
|
||||
elif not callable(f):
|
||||
raise NoSuchFunction(self.NOT_FOUND,
|
||||
"procedure %s not callable" % procedurePath)
|
||||
else:
|
||||
return f
|
||||
|
||||
def listProcedures(self):
|
||||
"""
|
||||
Return a list of the names of all xmlrpc procedures.
|
||||
|
||||
@since: 11.1
|
||||
"""
|
||||
return reflect.prefixedMethodNames(self.__class__, 'xmlrpc_')
|
||||
|
||||
|
||||
class XMLRPCIntrospection(XMLRPC):
|
||||
"""
|
||||
Implement the XML-RPC Introspection API.
|
||||
|
||||
By default, the methodHelp method returns the 'help' method attribute,
|
||||
if it exists, otherwise the __doc__ method attribute, if it exists,
|
||||
otherwise the empty string.
|
||||
|
||||
To enable the methodSignature method, add a 'signature' method attribute
|
||||
containing a list of lists. See methodSignature's documentation for the
|
||||
format. Note the type strings should be XML-RPC types, not Python types.
|
||||
"""
|
||||
|
||||
def __init__(self, parent):
|
||||
"""
|
||||
Implement Introspection support for an XMLRPC server.
|
||||
|
||||
@param parent: the XMLRPC server to add Introspection support to.
|
||||
@type parent: L{XMLRPC}
|
||||
"""
|
||||
XMLRPC.__init__(self)
|
||||
self._xmlrpc_parent = parent
|
||||
|
||||
def xmlrpc_listMethods(self):
|
||||
"""
|
||||
Return a list of the method names implemented by this server.
|
||||
"""
|
||||
functions = []
|
||||
todo = [(self._xmlrpc_parent, '')]
|
||||
while todo:
|
||||
obj, prefix = todo.pop(0)
|
||||
functions.extend([prefix + name for name in obj.listProcedures()])
|
||||
todo.extend([ (obj.getSubHandler(name),
|
||||
prefix + name + obj.separator)
|
||||
for name in obj.getSubHandlerPrefixes() ])
|
||||
return functions
|
||||
|
||||
xmlrpc_listMethods.signature = [['array']]
|
||||
|
||||
def xmlrpc_methodHelp(self, method):
|
||||
"""
|
||||
Return a documentation string describing the use of the given method.
|
||||
"""
|
||||
method = self._xmlrpc_parent.lookupProcedure(method)
|
||||
return (getattr(method, 'help', None)
|
||||
or getattr(method, '__doc__', None) or '')
|
||||
|
||||
xmlrpc_methodHelp.signature = [['string', 'string']]
|
||||
|
||||
def xmlrpc_methodSignature(self, method):
|
||||
"""
|
||||
Return a list of type signatures.
|
||||
|
||||
Each type signature is a list of the form [rtype, type1, type2, ...]
|
||||
where rtype is the return type and typeN is the type of the Nth
|
||||
argument. If no signature information is available, the empty
|
||||
string is returned.
|
||||
"""
|
||||
method = self._xmlrpc_parent.lookupProcedure(method)
|
||||
return getattr(method, 'signature', None) or ''
|
||||
|
||||
xmlrpc_methodSignature.signature = [['array', 'string'],
|
||||
['string', 'string']]
|
||||
|
||||
|
||||
def addIntrospection(xmlrpc):
|
||||
"""
|
||||
Add Introspection support to an XMLRPC server.
|
||||
|
||||
@param parent: the XMLRPC server to add Introspection support to.
|
||||
@type parent: L{XMLRPC}
|
||||
"""
|
||||
xmlrpc.putSubHandler('system', XMLRPCIntrospection(xmlrpc))
|
||||
|
||||
|
||||
class QueryProtocol(http.HTTPClient):
|
||||
def connectionMade(self):
|
||||
self._response = None
|
||||
self.sendCommand(b'POST', self.factory.path)
|
||||
self.sendHeader(b'User-Agent', b'Twisted/XMLRPClib')
|
||||
self.sendHeader(b'Host', self.factory.host)
|
||||
self.sendHeader(b'Content-type', b'text/xml; charset=utf-8')
|
||||
payload = self.factory.payload
|
||||
self.sendHeader(b'Content-length', intToBytes(len(payload)))
|
||||
|
||||
if self.factory.user:
|
||||
auth = b':'.join([self.factory.user, self.factory.password])
|
||||
authHeader = b''.join([b'Basic ', base64.b64encode(auth)])
|
||||
self.sendHeader(b'Authorization', authHeader)
|
||||
self.endHeaders()
|
||||
self.transport.write(payload)
|
||||
|
||||
def handleStatus(self, version, status, message):
|
||||
if status != b'200':
|
||||
self.factory.badStatus(status, message)
|
||||
|
||||
def handleResponse(self, contents):
|
||||
"""
|
||||
Handle the XML-RPC response received from the server.
|
||||
|
||||
Specifically, disconnect from the server and store the XML-RPC
|
||||
response so that it can be properly handled when the disconnect is
|
||||
finished.
|
||||
"""
|
||||
self.transport.loseConnection()
|
||||
self._response = contents
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
The connection to the server has been lost.
|
||||
|
||||
If we have a full response from the server, then parse it and fired a
|
||||
Deferred with the return value or C{Fault} that the server gave us.
|
||||
"""
|
||||
http.HTTPClient.connectionLost(self, reason)
|
||||
if self._response is not None:
|
||||
response, self._response = self._response, None
|
||||
self.factory.parseResponse(response)
|
||||
|
||||
|
||||
payloadTemplate = """<?xml version="1.0"?>
|
||||
<methodCall>
|
||||
<methodName>%s</methodName>
|
||||
%s
|
||||
</methodCall>
|
||||
"""
|
||||
|
||||
|
||||
class _QueryFactory(protocol.ClientFactory):
|
||||
"""
|
||||
XML-RPC Client Factory
|
||||
|
||||
@ivar path: The path portion of the URL to which to post method calls.
|
||||
@type path: L{bytes}
|
||||
|
||||
@ivar host: The value to use for the Host HTTP header.
|
||||
@type host: L{bytes}
|
||||
|
||||
@ivar user: The username with which to authenticate with the server
|
||||
when making calls.
|
||||
@type user: L{bytes} or L{None}
|
||||
|
||||
@ivar password: The password with which to authenticate with the server
|
||||
when making calls.
|
||||
@type password: L{bytes} or L{None}
|
||||
|
||||
@ivar useDateTime: Accept datetime values as datetime.datetime objects.
|
||||
also passed to the underlying xmlrpclib implementation. Defaults to
|
||||
C{False}.
|
||||
@type useDateTime: C{bool}
|
||||
"""
|
||||
|
||||
deferred = None
|
||||
protocol = QueryProtocol
|
||||
|
||||
def __init__(self, path, host, method, user=None, password=None,
|
||||
allowNone=False, args=(), canceller=None, useDateTime=False):
|
||||
"""
|
||||
@param method: The name of the method to call.
|
||||
@type method: C{str}
|
||||
|
||||
@param allowNone: allow the use of None values in parameters. It's
|
||||
passed to the underlying xmlrpclib implementation. Defaults to
|
||||
C{False}.
|
||||
@type allowNone: C{bool} or L{None}
|
||||
|
||||
@param args: the arguments to pass to the method.
|
||||
@type args: C{tuple}
|
||||
|
||||
@param canceller: A 1-argument callable passed to the deferred as the
|
||||
canceller callback.
|
||||
@type canceller: callable or L{None}
|
||||
"""
|
||||
self.path, self.host = path, host
|
||||
self.user, self.password = user, password
|
||||
self.payload = payloadTemplate % (method,
|
||||
xmlrpclib.dumps(args, allow_none=allowNone))
|
||||
if isinstance(self.payload, unicode):
|
||||
self.payload = self.payload.encode('utf8')
|
||||
self.deferred = defer.Deferred(canceller)
|
||||
self.useDateTime = useDateTime
|
||||
|
||||
def parseResponse(self, contents):
|
||||
if not self.deferred:
|
||||
return
|
||||
try:
|
||||
response = xmlrpclib.loads(contents,
|
||||
use_datetime=self.useDateTime)[0][0]
|
||||
except:
|
||||
deferred, self.deferred = self.deferred, None
|
||||
deferred.errback(failure.Failure())
|
||||
else:
|
||||
deferred, self.deferred = self.deferred, None
|
||||
deferred.callback(response)
|
||||
|
||||
def clientConnectionLost(self, _, reason):
|
||||
if self.deferred is not None:
|
||||
deferred, self.deferred = self.deferred, None
|
||||
deferred.errback(reason)
|
||||
|
||||
clientConnectionFailed = clientConnectionLost
|
||||
|
||||
def badStatus(self, status, message):
|
||||
deferred, self.deferred = self.deferred, None
|
||||
deferred.errback(ValueError(status, message))
|
||||
|
||||
|
||||
|
||||
class Proxy:
|
||||
"""
|
||||
A Proxy for making remote XML-RPC calls.
|
||||
|
||||
Pass the URL of the remote XML-RPC server to the constructor.
|
||||
|
||||
Use C{proxy.callRemote('foobar', *args)} to call remote method
|
||||
'foobar' with *args.
|
||||
|
||||
@ivar user: The username with which to authenticate with the server
|
||||
when making calls. If specified, overrides any username information
|
||||
embedded in C{url}. If not specified, a value may be taken from
|
||||
C{url} if present.
|
||||
@type user: L{bytes} or L{None}
|
||||
|
||||
@ivar password: The password with which to authenticate with the server
|
||||
when making calls. If specified, overrides any password information
|
||||
embedded in C{url}. If not specified, a value may be taken from
|
||||
C{url} if present.
|
||||
@type password: L{bytes} or L{None}
|
||||
|
||||
@ivar allowNone: allow the use of None values in parameters. It's
|
||||
passed to the underlying L{xmlrpclib} implementation. Defaults to
|
||||
C{False}.
|
||||
@type allowNone: C{bool} or L{None}
|
||||
|
||||
@ivar useDateTime: Accept datetime values as datetime.datetime objects.
|
||||
also passed to the underlying L{xmlrpclib} implementation. Defaults to
|
||||
C{False}.
|
||||
@type useDateTime: C{bool}
|
||||
|
||||
@ivar connectTimeout: Number of seconds to wait before assuming the
|
||||
connection has failed.
|
||||
@type connectTimeout: C{float}
|
||||
|
||||
@ivar _reactor: The reactor used to create connections.
|
||||
@type _reactor: Object providing L{twisted.internet.interfaces.IReactorTCP}
|
||||
|
||||
@ivar queryFactory: Object returning a factory for XML-RPC protocol. Mainly
|
||||
useful for tests.
|
||||
"""
|
||||
queryFactory = _QueryFactory
|
||||
|
||||
def __init__(self, url, user=None, password=None, allowNone=False,
|
||||
useDateTime=False, connectTimeout=30.0, reactor=reactor):
|
||||
"""
|
||||
@param url: The URL to which to post method calls. Calls will be made
|
||||
over SSL if the scheme is HTTPS. If netloc contains username or
|
||||
password information, these will be used to authenticate, as long as
|
||||
the C{user} and C{password} arguments are not specified.
|
||||
@type url: L{bytes}
|
||||
|
||||
"""
|
||||
scheme, netloc, path, params, query, fragment = urllib_parse.urlparse(
|
||||
url)
|
||||
netlocParts = netloc.split(b'@')
|
||||
if len(netlocParts) == 2:
|
||||
userpass = netlocParts.pop(0).split(b':')
|
||||
self.user = userpass.pop(0)
|
||||
try:
|
||||
self.password = userpass.pop(0)
|
||||
except:
|
||||
self.password = None
|
||||
else:
|
||||
self.user = self.password = None
|
||||
hostport = netlocParts[0].split(b':')
|
||||
self.host = hostport.pop(0)
|
||||
try:
|
||||
self.port = int(hostport.pop(0))
|
||||
except:
|
||||
self.port = None
|
||||
self.path = path
|
||||
if self.path in [b'', None]:
|
||||
self.path = b'/'
|
||||
self.secure = (scheme == b'https')
|
||||
if user is not None:
|
||||
self.user = user
|
||||
if password is not None:
|
||||
self.password = password
|
||||
self.allowNone = allowNone
|
||||
self.useDateTime = useDateTime
|
||||
self.connectTimeout = connectTimeout
|
||||
self._reactor = reactor
|
||||
|
||||
|
||||
def callRemote(self, method, *args):
|
||||
"""
|
||||
Call remote XML-RPC C{method} with given arguments.
|
||||
|
||||
@return: a L{defer.Deferred} that will fire with the method response,
|
||||
or a failure if the method failed. Generally, the failure type will
|
||||
be L{Fault}, but you can also have an C{IndexError} on some buggy
|
||||
servers giving empty responses.
|
||||
|
||||
If the deferred is cancelled before the request completes, the
|
||||
connection is closed and the deferred will fire with a
|
||||
L{defer.CancelledError}.
|
||||
"""
|
||||
def cancel(d):
|
||||
factory.deferred = None
|
||||
connector.disconnect()
|
||||
factory = self.queryFactory(
|
||||
self.path, self.host, method, self.user,
|
||||
self.password, self.allowNone, args, cancel, self.useDateTime)
|
||||
|
||||
if self.secure:
|
||||
from twisted.internet import ssl
|
||||
connector = self._reactor.connectSSL(
|
||||
nativeString(self.host), self.port or 443,
|
||||
factory, ssl.ClientContextFactory(),
|
||||
timeout=self.connectTimeout)
|
||||
else:
|
||||
connector = self._reactor.connectTCP(
|
||||
nativeString(self.host), self.port or 80, factory,
|
||||
timeout=self.connectTimeout)
|
||||
return factory.deferred
|
||||
|
||||
|
||||
__all__ = [
|
||||
"XMLRPC", "Handler", "NoSuchFunction", "Proxy",
|
||||
|
||||
"Fault", "Binary", "Boolean", "DateTime"]
|
||||
Reference in New Issue
Block a user