1
# -*- test-case-name: twisted.web2.test.test_server -*-
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
"""This is a web-sever which integrates with the twisted.internet
11
import cStringIO as StringIO
13
import cgi, time, urlparse
14
from urllib import quote, unquote
15
from urlparse import urlsplit
19
from zope.interface import implements
21
from twisted.internet import defer
22
from twisted.python import log, failure
25
from twisted.web2 import http, iweb, fileupload, responsecode
26
from twisted.web2 import http_headers
27
from twisted.web2.filter.range import rangefilter
28
from twisted.web2 import error
30
from twisted.web2 import version as web2_version
31
from twisted import __version__ as twisted_version
33
VERSION = "Twisted/%s TwistedWeb/%s" % (twisted_version, web2_version)
34
_errorMarker = object()
37
def defaultHeadersFilter(request, response):
38
if not response.headers.hasHeader('server'):
39
response.headers.setHeader('server', VERSION)
40
if not response.headers.hasHeader('date'):
41
response.headers.setHeader('date', time.time())
43
defaultHeadersFilter.handleErrors = True
45
def preconditionfilter(request, response):
46
if request.method in ("GET", "HEAD"):
47
http.checkPreconditions(request, response)
51
request = iweb.IRequest(request)
52
txt = "%s %s HTTP/%d.%d\r\n" % (request.method, request.uri,
53
request.clientproto[0], request.clientproto[1])
56
for name, valuelist in request.headers.getAllRawHeaders():
57
for value in valuelist:
58
l.append("%s: %s\r\n" % (name, value))
63
{'content-type': http_headers.MimeType('message', 'http')},
66
def parsePOSTData(request):
67
if request.stream.length == 0:
68
return defer.succeed(None)
71
ctype = request.headers.getHeader('content-type')
74
return defer.succeed(None)
78
request.args.update(args)
80
def updateArgsAndFiles(data):
82
request.args.update(args)
83
request.files.update(files)
86
f.trap(fileupload.MimeFormatError)
87
raise http.HTTPError(responsecode.BAD_REQUEST)
89
if ctype.mediaType == 'application' and ctype.mediaSubtype == 'x-www-form-urlencoded':
90
d = fileupload.parse_urlencoded(request.stream)
91
d.addCallbacks(updateArgs, error)
93
elif ctype.mediaType == 'multipart' and ctype.mediaSubtype == 'form-data':
94
boundary = ctype.params.get('boundary')
96
return failure.Failure(fileupload.MimeFormatError("Boundary not specified in Content-Type."))
97
d = fileupload.parseMultipartFormData(request.stream, boundary)
98
d.addCallbacks(updateArgsAndFiles, error)
101
raise http.HTTPError(responsecode.BAD_REQUEST)
103
class StopTraversal(object):
105
Indicates to Request._handleSegment that it should stop handling
111
class Request(http.Request):
131
@ivar path: The path only (arguments not included).
132
@ivar args: All of the arguments, including URL and POST arguments.
133
@type args: A mapping of strings (the argument names) to lists of values.
134
i.e., ?foo=bar&foo=baz&quux=spam results in
135
{'foo': ['bar', 'baz'], 'quux': ['spam']}.
138
implements(iweb.IRequest)
141
_initialprepath = None
142
responseFilters = [rangefilter, preconditionfilter,
143
error.defaultErrorHandler, defaultHeadersFilter]
145
def __init__(self, *args, **kw):
146
if kw.has_key('site'):
147
self.site = kw['site']
149
if kw.has_key('prepathuri'):
150
self._initialprepath = kw['prepathuri']
153
# Copy response filters from the class
154
self.responseFilters = self.responseFilters[:]
157
http.Request.__init__(self, *args, **kw)
159
def addResponseFilter(self, f, atEnd=False):
161
self.responseFilters.append(f)
163
self.responseFilters.insert(0, f)
165
def unparseURL(self, scheme=None, host=None, port=None,
166
path=None, params=None, querystring=None, fragment=None):
167
"""Turn the request path into a url string. For any pieces of
168
the url that are not specified, use the value from the
169
request. The arguments have the same meaning as the same named
170
attributes of Request."""
172
if scheme is None: scheme = self.scheme
173
if host is None: host = self.host
174
if port is None: port = self.port
175
if path is None: path = self.path
176
if params is None: params = self.params
177
if querystring is None: query = self.querystring
178
if fragment is None: fragment = ''
180
if port == http.defaultPortForScheme.get(scheme, 0):
183
hostport = host + ':' + str(port)
185
return urlparse.urlunparse((
186
scheme, hostport, path,
187
params, querystring, fragment))
190
if self.uri[0] == '/':
191
# Can't use urlparse for request_uri because urlparse
192
# wants to be given an absolute or relative URI, not just
193
# an abs_path, and thus gets '//foo' wrong.
194
self.scheme = self.host = self.path = self.params = self.querystring = ''
196
self.path, self.querystring = self.uri.split('?', 1)
200
self.path, self.params = self.path.split(';', 1)
202
# It is an absolute uri, use standard urlparse
203
(self.scheme, self.host, self.path,
204
self.params, self.querystring, fragment) = urlparse.urlparse(self.uri)
207
self.args = cgi.parse_qs(self.querystring, True)
211
path = map(unquote, self.path[1:].split('/'))
212
if self._initialprepath:
213
# We were given an initial prepath -- this is for supporting
214
# CGI-ish applications where part of the path has already
216
prepath = map(unquote, self._initialprepath[1:].split('/'))
218
if path[:len(prepath)] == prepath:
219
self.prepath = prepath
220
self.postpath = path[len(prepath):]
227
#print "_parseURL", self.uri, (self.uri, self.scheme, self.host, self.path, self.params, self.querystring)
229
def _fixupURLParts(self):
230
hostaddr, secure = self.chanRequest.getHostInfo()
232
self.scheme = ('http', 'https')[secure]
235
self.host, self.port = http.splitHostPort(self.scheme, self.host)
237
# If GET line wasn't an absolute URL
238
host = self.headers.getHeader('host')
240
self.host, self.port = http.splitHostPort(self.scheme, host)
242
# When no hostname specified anywhere, either raise an
243
# error, or use the interface hostname, depending on
245
if self.clientproto >= (1,1):
246
raise http.HTTPError(responsecode.BAD_REQUEST)
247
self.host = hostaddr.host
248
self.port = hostaddr.port
255
resp = self.preprocessRequest()
257
self._cbFinishRender(resp).addErrback(self._processingFailed)
260
self._fixupURLParts()
261
self.remoteAddr = self.chanRequest.getRemoteHost()
263
failedDeferred = self._processingFailed(failure.Failure())
267
d.addCallback(self._getChild, self.site.resource, self.postpath)
268
d.addCallback(lambda res, req: res.renderHTTP(req), self)
269
d.addCallback(self._cbFinishRender)
270
d.addErrback(self._processingFailed)
273
def preprocessRequest(self):
274
"""Do any request processing that doesn't follow the normal
275
resource lookup procedure. "OPTIONS *" is handled here, for
276
example. This would also be the place to do any CONNECT
279
if self.method == "OPTIONS" and self.uri == "*":
280
response = http.Response(responsecode.OK)
281
response.headers.setHeader('allow', ('GET', 'HEAD', 'OPTIONS', 'TRACE'))
283
# This is where CONNECT would go if we wanted it
286
def _getChild(self, _, res, path, updatepaths=True):
287
"""Call res.locateChild, and pass the result on to _handleSegment."""
289
self.resources.append(res)
294
result = res.locateChild(self, path)
295
if isinstance(result, defer.Deferred):
296
return result.addCallback(self._handleSegment, res, path, updatepaths)
298
return self._handleSegment(result, res, path, updatepaths)
300
def _handleSegment(self, result, res, path, updatepaths):
301
"""Handle the result of a locateChild call done in _getChild."""
303
newres, newpath = result
304
# If the child resource is None then display a error page
306
raise http.HTTPError(responsecode.NOT_FOUND)
308
# If we got a deferred then we need to call back later, once the
309
# child is actually available.
310
if isinstance(newres, defer.Deferred):
311
return newres.addCallback(
312
lambda actualRes: self._handleSegment(
313
(actualRes, newpath), res, path, updatepaths)
317
url = quote("/" + "/".join(path))
321
if newpath is StopTraversal:
322
# We need to rethink how to do this.
324
self._rememberResource(res, url)
327
# raise ValueError("locateChild must not return StopTraversal with a resource other than self.")
329
newres = iweb.IResource(newres)
331
assert not newpath is path, "URL traversal cycle detected when attempting to locateChild %r from resource %r." % (path, res)
332
assert len(newpath) < len(path), "Infinite loop impending..."
335
# We found a Resource... update the request.prepath and postpath
336
for x in xrange(len(path) - len(newpath)):
337
self.prepath.append(self.postpath.pop(0))
339
child = self._getChild(None, newres, newpath, updatepaths=updatepaths)
340
self._rememberResource(child, url)
344
_urlsByResource = weakref.WeakKeyDictionary()
346
def _rememberResource(self, resource, url):
348
Remember the URL of a visited resource.
350
self._urlsByResource[resource] = url
353
def urlForResource(self, resource):
355
Looks up the URL of the given resource if this resource was found while
356
processing this request. Specifically, this includes the requested
357
resource, and resources looked up via L{locateResource}.
359
Note that a resource may be found at multiple URIs; if the same resource
360
is visited at more than one location while processing this request,
361
this method will return one of those URLs, but which one is not defined,
362
nor whether the same URL is returned in subsequent calls.
364
@param resource: the resource to find a URI for. This resource must
365
have been obtained from the request (ie. via its C{uri} attribute, or
366
through its C{locateResource} or C{locateChildResource} methods).
367
@return: a valid URL for C{resource} in this request.
368
@raise NoURLForResourceError: if C{resource} has no URL in this request
369
(because it was not obtained from the request).
371
resource = self._urlsByResource.get(resource, None)
373
raise NoURLForResourceError(resource)
376
def locateResource(self, url):
378
Looks up the resource with the given URL.
379
@param uri: The URL of the desired resource.
380
@return: a L{Deferred} resulting in the L{IResource} at the
381
given URL or C{None} if no such resource can be located.
382
@raise HTTPError: If C{url} is not a URL on the site that this
383
request is being applied to. The contained response will
384
have a status code of L{responsecode.BAD_GATEWAY}.
385
@raise HTTPError: If C{url} contains a query or fragment.
386
The contained response will have a status code of
387
L{responsecode.BAD_REQUEST}.
389
if url is None: return None
394
(scheme, host, path, query, fragment) = urlsplit(url)
396
if query or fragment:
397
raise http.HTTPError(http.StatusResponse(
398
responsecode.BAD_REQUEST,
399
"URL may not contain a query or fragment: %s" % (url,)
402
# The caller shouldn't be asking a request on one server to lookup a
403
# resource on some other server.
404
if (scheme and scheme != self.scheme) or (host and host != self.headers.getHeader("host")):
405
raise http.HTTPError(http.StatusResponse(
406
responsecode.BAD_GATEWAY,
407
"URL is not on this site (%s://%s/): %s" % (scheme, self.headers.getHeader("host"), url)
410
segments = path.split("/")
411
assert segments[0] == "", "URL path didn't begin with '/': %s" % (path,)
412
segments = map(unquote, segments[1:])
415
f.trap(http.HTTPError)
416
if f.value.response.code != responsecode.NOT_FOUND:
420
d = defer.maybeDeferred(self._getChild, None, self.site.resource, segments, updatepaths=False)
421
d.addCallback(self._rememberResource, path)
422
d.addErrback(notFound)
425
def locateChildResource(self, parent, childName):
427
Looks up the child resource with the given name given the parent
428
resource. This is similar to locateResource(), but doesn't have to
429
start the lookup from the root resource, so it is potentially faster.
430
@param parent: the parent of the resource being looked up. This resource
431
must have been obtained from the request (ie. via its C{uri} attribute,
432
or through its C{locateResource} or C{locateChildResource} methods).
433
@param childName: the name of the child of C{parent} to looked up.
435
@return: a L{Deferred} resulting in the L{IResource} at the
436
given URL or C{None} if no such resource can be located.
437
@raise NoURLForResourceError: if C{resource} was not obtained from the
440
if parent is None or childName is None:
443
assert "/" not in childName, "Child name may not contain '/': %s" % (childName,)
445
parentURL = self.urlForResource(parent)
446
if not parentURL.endswith("/"):
448
url = parentURL + quote(childName)
453
f.trap(http.HTTPError)
454
if f.value.response.code != responsecode.NOT_FOUND:
458
d = defer.maybeDeferred(self._getChild, None, parent, [segment], updatepaths=False)
459
d.addCallback(self._rememberResource, url)
460
d.addErrback(notFound)
463
def _processingFailed(self, reason):
464
if reason.check(http.HTTPError) is not None:
465
# If the exception was an HTTPError, leave it alone
466
d = defer.succeed(reason.value.response)
468
# Otherwise, it was a random exception, so give a
469
# ICanHandleException implementer a chance to render the page.
470
def _processingFailed_inner(reason):
471
handler = iweb.ICanHandleException(self, self)
472
return handler.renderHTTP_exception(self, reason)
473
d = defer.maybeDeferred(_processingFailed_inner, reason)
475
d.addCallback(self._cbFinishRender)
476
d.addErrback(self._processingReallyFailed, reason)
479
def _processingReallyFailed(self, reason, origReason):
480
log.msg("Exception rendering error page:", isErr=1)
482
log.msg("Original exception:", isErr=1)
485
body = ("<html><head><title>Internal Server Error</title></head>"
486
"<body><h1>Internal Server Error</h1>An error occurred rendering the requested page. Additionally, an error occured rendering the error page.</body></html>")
488
response = http.Response(
489
responsecode.INTERNAL_SERVER_ERROR,
490
{'content-type': http_headers.MimeType('text','html')},
492
self.writeResponse(response)
494
def _cbFinishRender(self, result):
495
def filterit(response, f):
496
if (hasattr(f, 'handleErrors') or
497
(response.code >= 200 and response.code < 300 and response.code != 204)):
498
return f(self, response)
502
response = iweb.IResponse(result, None)
505
for f in self.responseFilters:
506
d.addCallback(filterit, f)
507
d.addCallback(self.writeResponse)
511
resource = iweb.IResource(result, None)
513
self.resources.append(resource)
514
d = defer.maybeDeferred(resource.renderHTTP, self)
515
d.addCallback(self._cbFinishRender)
518
raise TypeError("html is not a resource or a response")
520
def renderHTTP_exception(self, req, reason):
521
log.msg("Exception rendering:", isErr=1)
524
body = ("<html><head><title>Internal Server Error</title></head>"
525
"<body><h1>Internal Server Error</h1>An error occurred rendering the requested page. More information is available in the server log.</body></html>")
527
return http.Response(
528
responsecode.INTERNAL_SERVER_ERROR,
529
{'content-type': http_headers.MimeType('text','html')},
533
def __init__(self, resource):
536
self.resource = iweb.IResource(resource)
538
def __call__(self, *args, **kwargs):
539
return Request(site=self, *args, **kwargs)
542
class NoURLForResourceError(RuntimeError):
543
def __init__(self, resource):
544
RuntimeError.__init__(self, "Resource %r has no URL in this request." % (resource,))
545
self.resource = resource
548
__all__ = ['Request', 'Site', 'StopTraversal', 'VERSION', 'defaultHeadersFilter', 'doTrace', 'parsePOSTData', 'preconditionfilter', 'NoURLForResourceError']