~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/web2/server.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.web2.test.test_server -*-
 
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
 
 
6
"""This is a web-sever which integrates with the twisted.internet
 
7
infrastructure.
 
8
"""
 
9
 
 
10
# System Imports
 
11
import cStringIO as StringIO
 
12
 
 
13
import cgi, time, urlparse
 
14
from urllib import quote, unquote
 
15
from urlparse import urlsplit
 
16
 
 
17
import weakref
 
18
 
 
19
from zope.interface import implements
 
20
# Twisted Imports
 
21
from twisted.internet import defer
 
22
from twisted.python import log, failure
 
23
 
 
24
# Sibling Imports
 
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
 
29
 
 
30
from twisted.web2 import version as web2_version
 
31
from twisted import __version__ as twisted_version
 
32
 
 
33
VERSION = "Twisted/%s TwistedWeb/%s" % (twisted_version, web2_version)
 
34
_errorMarker = object()
 
35
 
 
36
 
 
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())
 
42
    return response
 
43
defaultHeadersFilter.handleErrors = True
 
44
 
 
45
def preconditionfilter(request, response):
 
46
    if request.method in ("GET", "HEAD"):
 
47
        http.checkPreconditions(request, response)
 
48
    return response
 
49
 
 
50
def doTrace(request):
 
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])
 
54
 
 
55
    l=[]
 
56
    for name, valuelist in request.headers.getAllRawHeaders():
 
57
        for value in valuelist:
 
58
            l.append("%s: %s\r\n" % (name, value))
 
59
    txt += ''.join(l)
 
60
 
 
61
    return http.Response(
 
62
        responsecode.OK,
 
63
        {'content-type': http_headers.MimeType('message', 'http')}, 
 
64
        txt)
 
65
 
 
66
def parsePOSTData(request):
 
67
    if request.stream.length == 0:
 
68
        return defer.succeed(None)
 
69
    
 
70
    parser = None
 
71
    ctype = request.headers.getHeader('content-type')
 
72
 
 
73
    if ctype is None:
 
74
        return defer.succeed(None)
 
75
 
 
76
    def updateArgs(data):
 
77
        args = data
 
78
        request.args.update(args)
 
79
 
 
80
    def updateArgsAndFiles(data):
 
81
        args, files = data
 
82
        request.args.update(args)
 
83
        request.files.update(files)
 
84
 
 
85
    def error(f):
 
86
        f.trap(fileupload.MimeFormatError)
 
87
        raise http.HTTPError(responsecode.BAD_REQUEST)
 
88
    
 
89
    if ctype.mediaType == 'application' and ctype.mediaSubtype == 'x-www-form-urlencoded':
 
90
        d = fileupload.parse_urlencoded(request.stream)
 
91
        d.addCallbacks(updateArgs, error)
 
92
        return d
 
93
    elif ctype.mediaType == 'multipart' and ctype.mediaSubtype == 'form-data':
 
94
        boundary = ctype.params.get('boundary')
 
95
        if boundary is None:
 
96
            return failure.Failure(fileupload.MimeFormatError("Boundary not specified in Content-Type."))
 
97
        d = fileupload.parseMultipartFormData(request.stream, boundary)
 
98
        d.addCallbacks(updateArgsAndFiles, error)
 
99
        return d
 
100
    else:
 
101
        raise http.HTTPError(responsecode.BAD_REQUEST)
 
102
 
 
103
class StopTraversal(object):
 
104
    """
 
105
    Indicates to Request._handleSegment that it should stop handling
 
106
    path segments.
 
107
    """
 
108
    pass
 
109
 
 
110
 
 
111
class Request(http.Request):
 
112
    """
 
113
    vars:
 
114
    site
 
115
 
 
116
    remoteAddr
 
117
    
 
118
    scheme
 
119
    host
 
120
    port
 
121
    path
 
122
    params
 
123
    querystring
 
124
    
 
125
    args
 
126
    files
 
127
    
 
128
    prepath
 
129
    postpath
 
130
 
 
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']}.
 
136
 
 
137
    """
 
138
    implements(iweb.IRequest)
 
139
    
 
140
    site = None
 
141
    _initialprepath = None
 
142
    responseFilters = [rangefilter, preconditionfilter,
 
143
                       error.defaultErrorHandler, defaultHeadersFilter]
 
144
    
 
145
    def __init__(self, *args, **kw):
 
146
        if kw.has_key('site'):
 
147
            self.site = kw['site']
 
148
            del kw['site']
 
149
        if kw.has_key('prepathuri'):
 
150
            self._initialprepath = kw['prepathuri']
 
151
            del kw['prepathuri']
 
152
 
 
153
        # Copy response filters from the class
 
154
        self.responseFilters = self.responseFilters[:]
 
155
        self.files = {}
 
156
        self.resources = []
 
157
        http.Request.__init__(self, *args, **kw)
 
158
 
 
159
    def addResponseFilter(self, f, atEnd=False):
 
160
        if atEnd:
 
161
            self.responseFilters.append(f)
 
162
        else:
 
163
            self.responseFilters.insert(0, f)
 
164
 
 
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."""
 
171
        
 
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 = ''
 
179
        
 
180
        if port == http.defaultPortForScheme.get(scheme, 0):
 
181
            hostport = host
 
182
        else:
 
183
            hostport = host + ':' + str(port)
 
184
        
 
185
        return urlparse.urlunparse((
 
186
            scheme, hostport, path,
 
187
            params, querystring, fragment))
 
188
 
 
189
    def _parseURL(self):
 
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 = ''
 
195
            if '?' in self.uri:
 
196
                self.path, self.querystring = self.uri.split('?', 1)
 
197
            else:
 
198
                self.path = self.uri
 
199
            if ';' in self.path:
 
200
                self.path, self.params = self.path.split(';', 1)
 
201
        else:
 
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)
 
205
 
 
206
        if self.querystring:
 
207
            self.args = cgi.parse_qs(self.querystring, True)
 
208
        else:
 
209
            self.args = {}
 
210
        
 
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
 
215
            # been processed
 
216
            prepath = map(unquote, self._initialprepath[1:].split('/'))
 
217
            
 
218
            if path[:len(prepath)] == prepath:
 
219
                self.prepath = prepath
 
220
                self.postpath = path[len(prepath):]
 
221
            else:
 
222
                self.prepath = []
 
223
                self.postpath = path
 
224
        else:
 
225
            self.prepath = []
 
226
            self.postpath = path
 
227
        #print "_parseURL", self.uri, (self.uri, self.scheme, self.host, self.path, self.params, self.querystring)
 
228
 
 
229
    def _fixupURLParts(self):
 
230
        hostaddr, secure = self.chanRequest.getHostInfo()
 
231
        if not self.scheme:
 
232
            self.scheme = ('http', 'https')[secure]
 
233
            
 
234
        if self.host:
 
235
            self.host, self.port = http.splitHostPort(self.scheme, self.host)
 
236
        else:
 
237
            # If GET line wasn't an absolute URL
 
238
            host = self.headers.getHeader('host')
 
239
            if host:
 
240
                self.host, self.port = http.splitHostPort(self.scheme, host)
 
241
            else:
 
242
                # When no hostname specified anywhere, either raise an
 
243
                # error, or use the interface hostname, depending on
 
244
                # protocol version
 
245
                if self.clientproto >= (1,1):
 
246
                    raise http.HTTPError(responsecode.BAD_REQUEST)
 
247
                self.host = hostaddr.host
 
248
                self.port = hostaddr.port
 
249
 
 
250
 
 
251
    def process(self):
 
252
        "Process a request."
 
253
        try:
 
254
            self.checkExpect()
 
255
            resp = self.preprocessRequest()
 
256
            if resp is not None:
 
257
                self._cbFinishRender(resp).addErrback(self._processingFailed)
 
258
                return
 
259
            self._parseURL()
 
260
            self._fixupURLParts()
 
261
            self.remoteAddr = self.chanRequest.getRemoteHost()
 
262
        except:
 
263
            failedDeferred = self._processingFailed(failure.Failure())
 
264
            return
 
265
        
 
266
        d = defer.Deferred()
 
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)
 
271
        d.callback(None)
 
272
 
 
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
 
277
        processing."""
 
278
        
 
279
        if self.method == "OPTIONS" and self.uri == "*":
 
280
            response = http.Response(responsecode.OK)
 
281
            response.headers.setHeader('allow', ('GET', 'HEAD', 'OPTIONS', 'TRACE'))
 
282
            return response
 
283
        # This is where CONNECT would go if we wanted it
 
284
        return None
 
285
    
 
286
    def _getChild(self, _, res, path, updatepaths=True):
 
287
        """Call res.locateChild, and pass the result on to _handleSegment."""
 
288
 
 
289
        self.resources.append(res)
 
290
 
 
291
        if not path:
 
292
            return res
 
293
 
 
294
        result = res.locateChild(self, path)
 
295
        if isinstance(result, defer.Deferred):
 
296
            return result.addCallback(self._handleSegment, res, path, updatepaths)
 
297
        else:
 
298
            return self._handleSegment(result, res, path, updatepaths)
 
299
 
 
300
    def _handleSegment(self, result, res, path, updatepaths):
 
301
        """Handle the result of a locateChild call done in _getChild."""
 
302
 
 
303
        newres, newpath = result
 
304
        # If the child resource is None then display a error page
 
305
        if newres is None:
 
306
            raise http.HTTPError(responsecode.NOT_FOUND)
 
307
 
 
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)
 
314
                )
 
315
 
 
316
        if path:
 
317
            url = quote("/" + "/".join(path))
 
318
        else:
 
319
            url = "/"
 
320
 
 
321
        if newpath is StopTraversal:
 
322
            # We need to rethink how to do this.
 
323
            #if newres is res:
 
324
                self._rememberResource(res, url)
 
325
                return res
 
326
            #else:
 
327
            #    raise ValueError("locateChild must not return StopTraversal with a resource other than self.")
 
328
 
 
329
        newres = iweb.IResource(newres)
 
330
        if newres is res:
 
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..."
 
333
 
 
334
        if updatepaths:
 
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))
 
338
 
 
339
        child = self._getChild(None, newres, newpath, updatepaths=updatepaths)
 
340
        self._rememberResource(child, url)
 
341
 
 
342
        return child
 
343
 
 
344
    _urlsByResource = weakref.WeakKeyDictionary()
 
345
 
 
346
    def _rememberResource(self, resource, url):
 
347
        """
 
348
        Remember the URL of a visited resource.
 
349
        """
 
350
        self._urlsByResource[resource] = url
 
351
        return resource
 
352
 
 
353
    def urlForResource(self, resource):
 
354
        """
 
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}.
 
358
 
 
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.
 
363
 
 
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).
 
370
        """
 
371
        resource = self._urlsByResource.get(resource, None)
 
372
        if resource is None:
 
373
            raise NoURLForResourceError(resource)
 
374
        return resource
 
375
        
 
376
    def locateResource(self, url):
 
377
        """
 
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}.
 
388
        """
 
389
        if url is None: return None
 
390
 
 
391
        #
 
392
        # Parse the URL
 
393
        #
 
394
        (scheme, host, path, query, fragment) = urlsplit(url)
 
395
    
 
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,)
 
400
            ))
 
401
 
 
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)
 
408
            ))
 
409
 
 
410
        segments = path.split("/")
 
411
        assert segments[0] == "", "URL path didn't begin with '/': %s" % (path,)
 
412
        segments = map(unquote, segments[1:])
 
413
 
 
414
        def notFound(f):
 
415
            f.trap(http.HTTPError)
 
416
            if f.value.response.code != responsecode.NOT_FOUND:
 
417
                return f
 
418
            return None
 
419
 
 
420
        d = defer.maybeDeferred(self._getChild, None, self.site.resource, segments, updatepaths=False)
 
421
        d.addCallback(self._rememberResource, path)
 
422
        d.addErrback(notFound)
 
423
        return d
 
424
 
 
425
    def locateChildResource(self, parent, childName):
 
426
        """
 
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.
 
434
            to C{parent}.
 
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
 
438
            request.
 
439
        """
 
440
        if parent is None or childName is None:
 
441
            return None
 
442
 
 
443
        assert "/" not in childName, "Child name may not contain '/': %s" % (childName,)
 
444
 
 
445
        parentURL = self.urlForResource(parent)
 
446
        if not parentURL.endswith("/"):
 
447
            parentURL += "/"
 
448
        url = parentURL + quote(childName)
 
449
 
 
450
        segment = childName
 
451
 
 
452
        def notFound(f):
 
453
            f.trap(http.HTTPError)
 
454
            if f.value.response.code != responsecode.NOT_FOUND:
 
455
                return f
 
456
            return None
 
457
 
 
458
        d = defer.maybeDeferred(self._getChild, None, parent, [segment], updatepaths=False)
 
459
        d.addCallback(self._rememberResource, url)
 
460
        d.addErrback(notFound)
 
461
        return d
 
462
 
 
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)
 
467
        else:
 
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)
 
474
        
 
475
        d.addCallback(self._cbFinishRender)
 
476
        d.addErrback(self._processingReallyFailed, reason)
 
477
        return d
 
478
    
 
479
    def _processingReallyFailed(self, reason, origReason):
 
480
        log.msg("Exception rendering error page:", isErr=1)
 
481
        log.err(reason)
 
482
        log.msg("Original exception:", isErr=1)
 
483
        log.err(origReason)
 
484
        
 
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>")
 
487
        
 
488
        response = http.Response(
 
489
            responsecode.INTERNAL_SERVER_ERROR,
 
490
            {'content-type': http_headers.MimeType('text','html')},
 
491
            body)
 
492
        self.writeResponse(response)
 
493
 
 
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)
 
499
            else:
 
500
                return response
 
501
 
 
502
        response = iweb.IResponse(result, None)
 
503
        if response:
 
504
            d = defer.Deferred()
 
505
            for f in self.responseFilters:
 
506
                d.addCallback(filterit, f)
 
507
            d.addCallback(self.writeResponse)
 
508
            d.callback(response)
 
509
            return d
 
510
 
 
511
        resource = iweb.IResource(result, None)
 
512
        if resource:
 
513
            self.resources.append(resource)
 
514
            d = defer.maybeDeferred(resource.renderHTTP, self)
 
515
            d.addCallback(self._cbFinishRender)
 
516
            return d
 
517
 
 
518
        raise TypeError("html is not a resource or a response")
 
519
 
 
520
    def renderHTTP_exception(self, req, reason):
 
521
        log.msg("Exception rendering:", isErr=1)
 
522
        log.err(reason)
 
523
        
 
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>")
 
526
        
 
527
        return http.Response(
 
528
            responsecode.INTERNAL_SERVER_ERROR,
 
529
            {'content-type': http_headers.MimeType('text','html')},
 
530
            body)
 
531
 
 
532
class Site(object):
 
533
    def __init__(self, resource):
 
534
        """Initialize.
 
535
        """
 
536
        self.resource = iweb.IResource(resource)
 
537
 
 
538
    def __call__(self, *args, **kwargs):
 
539
        return Request(site=self, *args, **kwargs)
 
540
 
 
541
 
 
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
 
546
 
 
547
 
 
548
__all__ = ['Request', 'Site', 'StopTraversal', 'VERSION', 'defaultHeadersFilter', 'doTrace', 'parsePOSTData', 'preconditionfilter', 'NoURLForResourceError']