root/trunk/twisted/web/server.py

Revision 33885, 18.7 KB (checked in by exarkun, 2 months ago)

Mege setresponsecode-distrib-5525

Author: tom.prince
Reviewer: Fahrenheit, jesstess, exarkun
Fixes: #5525

Add the message parameter to the PB view of twisted.web.server.Request.
This allows distributed servers (twisted.web.distrib) to set the response
message, just as any normal HTTP code in a Twisted Web server may do.

Line 
1# -*- test-case-name: twisted.web.test.test_web -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5
6"""
7This is a web-server which integrates with the twisted.internet
8infrastructure.
9"""
10
11# System Imports
12
13import warnings
14import string
15import types
16import copy
17import os
18from urllib import quote
19
20from zope.interface import implements
21
22from urllib import unquote
23
24#some useful constants
25NOT_DONE_YET = 1
26
27# Twisted Imports
28from twisted.spread import pb
29from twisted.internet import address, task
30from twisted.web import iweb, http
31from twisted.python import log, reflect, failure, components
32from twisted import copyright
33from twisted.web import util as webutil, resource
34from twisted.web.error import UnsupportedMethod
35from twisted.web.microdom import escape
36
37from twisted.python.versions import Version
38from twisted.python.deprecate import deprecatedModuleAttribute
39
40
41__all__ = [
42    'supportedMethods',
43    'Request',
44    'Session',
45    'Site',
46    'version',
47    'NOT_DONE_YET'
48]
49
50
51# backwards compatability
52deprecatedModuleAttribute(
53    Version("Twisted", 12, 1, 0),
54    "Please use twisted.web.http.datetimeToString instead",
55    "twisted.web.server",
56    "date_time_string")
57deprecatedModuleAttribute(
58    Version("Twisted", 12, 1, 0),
59    "Please use twisted.web.http.stringToDatetime instead",
60    "twisted.web.server",
61    "string_date_time")
62date_time_string = http.datetimeToString
63string_date_time = http.stringToDatetime
64
65# Support for other methods may be implemented on a per-resource basis.
66supportedMethods = ('GET', 'HEAD', 'POST')
67
68
69def _addressToTuple(addr):
70    if isinstance(addr, address.IPv4Address):
71        return ('INET', addr.host, addr.port)
72    elif isinstance(addr, address.UNIXAddress):
73        return ('UNIX', addr.name)
74    else:
75        return tuple(addr)
76
77class Request(pb.Copyable, http.Request, components.Componentized):
78    """
79    An HTTP request.
80
81    @ivar defaultContentType: A C{str} giving the default I{Content-Type} value
82        to send in responses if no other value is set.  C{None} disables the
83        default.
84    """
85    implements(iweb.IRequest)
86
87    defaultContentType = "text/html"
88
89    site = None
90    appRootURL = None
91    __pychecker__ = 'unusednames=issuer'
92    _inFakeHead = False
93
94    def __init__(self, *args, **kw):
95        http.Request.__init__(self, *args, **kw)
96        components.Componentized.__init__(self)
97
98    def getStateToCopyFor(self, issuer):
99        x = self.__dict__.copy()
100        del x['transport']
101        # XXX refactor this attribute out; it's from protocol
102        # del x['server']
103        del x['channel']
104        del x['content']
105        del x['site']
106        self.content.seek(0, 0)
107        x['content_data'] = self.content.read()
108        x['remote'] = pb.ViewPoint(issuer, self)
109
110        # Address objects aren't jellyable
111        x['host'] = _addressToTuple(x['host'])
112        x['client'] = _addressToTuple(x['client'])
113
114        # Header objects also aren't jellyable.
115        x['requestHeaders'] = list(x['requestHeaders'].getAllRawHeaders())
116
117        return x
118
119    # HTML generation helpers
120
121    def sibLink(self, name):
122        "Return the text that links to a sibling of the requested resource."
123        if self.postpath:
124            return (len(self.postpath)*"../") + name
125        else:
126            return name
127
128    def childLink(self, name):
129        "Return the text that links to a child of the requested resource."
130        lpp = len(self.postpath)
131        if lpp > 1:
132            return ((lpp-1)*"../") + name
133        elif lpp == 1:
134            return name
135        else: # lpp == 0
136            if len(self.prepath) and self.prepath[-1]:
137                return self.prepath[-1] + '/' + name
138            else:
139                return name
140
141    def process(self):
142        "Process a request."
143
144        # get site from channel
145        self.site = self.channel.site
146
147        # set various default headers
148        self.setHeader('server', version)
149        self.setHeader('date', http.datetimeToString())
150
151        # Resource Identification
152        self.prepath = []
153        self.postpath = map(unquote, string.split(self.path[1:], '/'))
154        try:
155            resrc = self.site.getResourceFor(self)
156            self.render(resrc)
157        except:
158            self.processingFailed(failure.Failure())
159
160    def write(self, data):
161        """
162        Write data to the transport (if not responding to a HEAD request).
163
164        @param data: A string to write to the response.
165        """
166        if not self.startedWriting:
167            # Before doing the first write, check to see if a default
168            # Content-Type header should be supplied.
169            modified = self.code != http.NOT_MODIFIED
170            contentType = self.responseHeaders.getRawHeaders('content-type')
171            if modified and contentType is None and self.defaultContentType is not None:
172                self.responseHeaders.setRawHeaders(
173                    'content-type', [self.defaultContentType])
174
175        # Only let the write happen if we're not generating a HEAD response by
176        # faking out the request method.  Note, if we are doing that,
177        # startedWriting will never be true, and the above logic may run
178        # multiple times.  It will only actually change the responseHeaders once
179        # though, so it's still okay.
180        if not self._inFakeHead:
181            http.Request.write(self, data)
182
183
184    def render(self, resrc):
185        """
186        Ask a resource to render itself.
187
188        @param resrc: a L{twisted.web.resource.IResource}.
189        """
190        try:
191            body = resrc.render(self)
192        except UnsupportedMethod, e:
193            allowedMethods = e.allowedMethods
194            if (self.method == "HEAD") and ("GET" in allowedMethods):
195                # We must support HEAD (RFC 2616, 5.1.1).  If the
196                # resource doesn't, fake it by giving the resource
197                # a 'GET' request and then return only the headers,
198                # not the body.
199                log.msg("Using GET to fake a HEAD request for %s" %
200                        (resrc,))
201                self.method = "GET"
202                self._inFakeHead = True
203                body = resrc.render(self)
204
205                if body is NOT_DONE_YET:
206                    log.msg("Tried to fake a HEAD request for %s, but "
207                            "it got away from me." % resrc)
208                    # Oh well, I guess we won't include the content length.
209                else:
210                    self.setHeader('content-length', str(len(body)))
211
212                self._inFakeHead = False
213                self.method = "HEAD"
214                self.write('')
215                self.finish()
216                return
217
218            if self.method in (supportedMethods):
219                # We MUST include an Allow header
220                # (RFC 2616, 10.4.6 and 14.7)
221                self.setHeader('Allow', ', '.join(allowedMethods))
222                s = ('''Your browser approached me (at %(URI)s) with'''
223                     ''' the method "%(method)s".  I only allow'''
224                     ''' the method%(plural)s %(allowed)s here.''' % {
225                    'URI': escape(self.uri),
226                    'method': self.method,
227                    'plural': ((len(allowedMethods) > 1) and 's') or '',
228                    'allowed': string.join(allowedMethods, ', ')
229                    })
230                epage = resource.ErrorPage(http.NOT_ALLOWED,
231                                           "Method Not Allowed", s)
232                body = epage.render(self)
233            else:
234                epage = resource.ErrorPage(
235                    http.NOT_IMPLEMENTED, "Huh?",
236                    "I don't know how to treat a %s request." %
237                    (escape(self.method),))
238                body = epage.render(self)
239        # end except UnsupportedMethod
240
241        if body == NOT_DONE_YET:
242            return
243        if type(body) is not types.StringType:
244            body = resource.ErrorPage(
245                http.INTERNAL_SERVER_ERROR,
246                "Request did not return a string",
247                "Request: " + html.PRE(reflect.safe_repr(self)) + "<br />" +
248                "Resource: " + html.PRE(reflect.safe_repr(resrc)) + "<br />" +
249                "Value: " + html.PRE(reflect.safe_repr(body))).render(self)
250
251        if self.method == "HEAD":
252            if len(body) > 0:
253                # This is a Bad Thing (RFC 2616, 9.4)
254                log.msg("Warning: HEAD request %s for resource %s is"
255                        " returning a message body."
256                        "  I think I'll eat it."
257                        % (self, resrc))
258                self.setHeader('content-length', str(len(body)))
259            self.write('')
260        else:
261            self.setHeader('content-length', str(len(body)))
262            self.write(body)
263        self.finish()
264
265    def processingFailed(self, reason):
266        log.err(reason)
267        if self.site.displayTracebacks:
268            body = ("<html><head><title>web.Server Traceback (most recent call last)</title></head>"
269                    "<body><b>web.Server Traceback (most recent call last):</b>\n\n"
270                    "%s\n\n</body></html>\n"
271                    % webutil.formatFailure(reason))
272        else:
273            body = ("<html><head><title>Processing Failed</title></head><body>"
274                  "<b>Processing Failed</b></body></html>")
275
276        self.setResponseCode(http.INTERNAL_SERVER_ERROR)
277        self.setHeader('content-type',"text/html")
278        self.setHeader('content-length', str(len(body)))
279        self.write(body)
280        self.finish()
281        return reason
282
283    def view_write(self, issuer, data):
284        """Remote version of write; same interface.
285        """
286        self.write(data)
287
288    def view_finish(self, issuer):
289        """Remote version of finish; same interface.
290        """
291        self.finish()
292
293    def view_addCookie(self, issuer, k, v, **kwargs):
294        """Remote version of addCookie; same interface.
295        """
296        self.addCookie(k, v, **kwargs)
297
298    def view_setHeader(self, issuer, k, v):
299        """Remote version of setHeader; same interface.
300        """
301        self.setHeader(k, v)
302
303    def view_setLastModified(self, issuer, when):
304        """Remote version of setLastModified; same interface.
305        """
306        self.setLastModified(when)
307
308    def view_setETag(self, issuer, tag):
309        """Remote version of setETag; same interface.
310        """
311        self.setETag(tag)
312
313
314    def view_setResponseCode(self, issuer, code, message=None):
315        """
316        Remote version of setResponseCode; same interface.
317        """
318        self.setResponseCode(code, message)
319
320
321    def view_registerProducer(self, issuer, producer, streaming):
322        """Remote version of registerProducer; same interface.
323        (requires a remote producer.)
324        """
325        self.registerProducer(_RemoteProducerWrapper(producer), streaming)
326
327    def view_unregisterProducer(self, issuer):
328        self.unregisterProducer()
329
330    ### these calls remain local
331
332    session = None
333
334    def getSession(self, sessionInterface = None):
335        # Session management
336        if not self.session:
337            cookiename = string.join(['TWISTED_SESSION'] + self.sitepath, "_")
338            sessionCookie = self.getCookie(cookiename)
339            if sessionCookie:
340                try:
341                    self.session = self.site.getSession(sessionCookie)
342                except KeyError:
343                    pass
344            # if it still hasn't been set, fix it up.
345            if not self.session:
346                self.session = self.site.makeSession()
347                self.addCookie(cookiename, self.session.uid, path='/')
348        self.session.touch()
349        if sessionInterface:
350            return self.session.getComponent(sessionInterface)
351        return self.session
352
353    def _prePathURL(self, prepath):
354        port = self.getHost().port
355        if self.isSecure():
356            default = 443
357        else:
358            default = 80
359        if port == default:
360            hostport = ''
361        else:
362            hostport = ':%d' % port
363        return 'http%s://%s%s/%s' % (
364            self.isSecure() and 's' or '',
365            self.getRequestHostname(),
366            hostport,
367            '/'.join([quote(segment, safe='') for segment in prepath]))
368
369    def prePathURL(self):
370        return self._prePathURL(self.prepath)
371
372    def URLPath(self):
373        from twisted.python import urlpath
374        return urlpath.URLPath.fromRequest(self)
375
376    def rememberRootURL(self):
377        """
378        Remember the currently-processed part of the URL for later
379        recalling.
380        """
381        url = self._prePathURL(self.prepath[:-1])
382        self.appRootURL = url
383
384    def getRootURL(self):
385        """
386        Get a previously-remembered URL.
387        """
388        return self.appRootURL
389
390
391class _RemoteProducerWrapper:
392    def __init__(self, remote):
393        self.resumeProducing = remote.remoteMethod("resumeProducing")
394        self.pauseProducing = remote.remoteMethod("pauseProducing")
395        self.stopProducing = remote.remoteMethod("stopProducing")
396
397
398class Session(components.Componentized):
399    """
400    A user's session with a system.
401
402    This utility class contains no functionality, but is used to
403    represent a session.
404
405    @ivar _reactor: An object providing L{IReactorTime} to use for scheduling
406        expiration.
407    @ivar sessionTimeout: timeout of a session, in seconds.
408    @ivar loopFactory: Deprecated in Twisted 9.0.  Does nothing.  Do not use.
409    """
410    sessionTimeout = 900
411    loopFactory = task.LoopingCall
412
413    _expireCall = None
414
415    def __init__(self, site, uid, reactor=None):
416        """
417        Initialize a session with a unique ID for that session.
418        """
419        components.Componentized.__init__(self)
420
421        if reactor is None:
422            from twisted.internet import reactor
423        self._reactor = reactor
424
425        self.site = site
426        self.uid = uid
427        self.expireCallbacks = []
428        self.touch()
429        self.sessionNamespaces = {}
430
431
432    def startCheckingExpiration(self, lifetime=None):
433        """
434        Start expiration tracking.
435
436        @param lifetime: Ignored; deprecated.
437
438        @return: C{None}
439        """
440        if lifetime is not None:
441            warnings.warn(
442                "The lifetime parameter to startCheckingExpiration is "
443                "deprecated since Twisted 9.0.  See Session.sessionTimeout "
444                "instead.", DeprecationWarning, stacklevel=2)
445        self._expireCall = self._reactor.callLater(
446            self.sessionTimeout, self.expire)
447
448
449    def notifyOnExpire(self, callback):
450        """
451        Call this callback when the session expires or logs out.
452        """
453        self.expireCallbacks.append(callback)
454
455
456    def expire(self):
457        """
458        Expire/logout of the session.
459        """
460        del self.site.sessions[self.uid]
461        for c in self.expireCallbacks:
462            c()
463        self.expireCallbacks = []
464        if self._expireCall and self._expireCall.active():
465            self._expireCall.cancel()
466            # Break reference cycle.
467            self._expireCall = None
468
469
470    def touch(self):
471        """
472        Notify session modification.
473        """
474        self.lastModified = self._reactor.seconds()
475        if self._expireCall is not None:
476            self._expireCall.reset(self.sessionTimeout)
477
478
479    def checkExpired(self):
480        """
481        Deprecated; does nothing.
482        """
483        warnings.warn(
484            "Session.checkExpired is deprecated since Twisted 9.0; sessions "
485            "check themselves now, you don't need to.",
486            stacklevel=2, category=DeprecationWarning)
487
488
489version = "TwistedWeb/%s" % copyright.version
490
491
492class Site(http.HTTPFactory):
493    """
494    A web site: manage log, sessions, and resources.
495
496    @ivar counter: increment value used for generating unique sessions ID.
497    @ivar requestFactory: factory creating requests objects. Default to
498        L{Request}.
499    @ivar displayTracebacks: if set, Twisted internal errors are displayed on
500        rendered pages. Default to C{True}.
501    @ivar sessionFactory: factory for sessions objects. Default to L{Session}.
502    @ivar sessionCheckTime: Deprecated.  See L{Session.sessionTimeout} instead.
503    """
504    counter = 0
505    requestFactory = Request
506    displayTracebacks = True
507    sessionFactory = Session
508    sessionCheckTime = 1800
509
510    def __init__(self, resource, logPath=None, timeout=60*60*12):
511        """
512        Initialize.
513        """
514        http.HTTPFactory.__init__(self, logPath=logPath, timeout=timeout)
515        self.sessions = {}
516        self.resource = resource
517
518    def _openLogFile(self, path):
519        from twisted.python import logfile
520        return logfile.LogFile(os.path.basename(path), os.path.dirname(path))
521
522    def __getstate__(self):
523        d = self.__dict__.copy()
524        d['sessions'] = {}
525        return d
526
527    def _mkuid(self):
528        """
529        (internal) Generate an opaque, unique ID for a user's session.
530        """
531        from twisted.python.hashlib import md5
532        import random
533        self.counter = self.counter + 1
534        return md5("%s_%s" % (str(random.random()) , str(self.counter))).hexdigest()
535
536    def makeSession(self):
537        """
538        Generate a new Session instance, and store it for future reference.
539        """
540        uid = self._mkuid()
541        session = self.sessions[uid] = self.sessionFactory(self, uid)
542        session.startCheckingExpiration()
543        return session
544
545    def getSession(self, uid):
546        """
547        Get a previously generated session, by its unique ID.
548        This raises a KeyError if the session is not found.
549        """
550        return self.sessions[uid]
551
552    def buildProtocol(self, addr):
553        """
554        Generate a channel attached to this site.
555        """
556        channel = http.HTTPFactory.buildProtocol(self, addr)
557        channel.requestFactory = self.requestFactory
558        channel.site = self
559        return channel
560
561    isLeaf = 0
562
563    def render(self, request):
564        """
565        Redirect because a Site is always a directory.
566        """
567        request.redirect(request.prePathURL() + '/')
568        request.finish()
569
570    def getChildWithDefault(self, pathEl, request):
571        """
572        Emulate a resource's getChild method.
573        """
574        request.site = self
575        return self.resource.getChildWithDefault(pathEl, request)
576
577    def getResourceFor(self, request):
578        """
579        Get a resource for a request.
580
581        This iterates through the resource heirarchy, calling
582        getChildWithDefault on each resource it finds for a path element,
583        stopping when it hits an element where isLeaf is true.
584        """
585        request.site = self
586        # Sitepath is used to determine cookie names between distributed
587        # servers and disconnected sites.
588        request.sitepath = copy.copy(request.prepath)
589        return resource.getChildForRequest(self.resource, request)
590
591
592import html
Note: See TracBrowser for help on using the browser.