~landscape/zope3/newer-from-ztk

« back to all changes in this revision

Viewing changes to src/twisted/web2/server.py

  • Committer: Thomas Hervé
  • Date: 2009-07-08 13:52:04 UTC
  • Revision ID: thomas@canonical.com-20090708135204-df5eesrthifpylf8
Remove twisted copy

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']