root / trunk / twisted / web / server.py

Revision 26710, 17.4 kB (checked in by exarkun, 3 months ago)

Merge distrib-request-headers-3697

Author: exarkun
Reviewer: therve, glyph
Fixes: #3697

Handle the new Headers instance at twisted.web.http.Request.requestHeaders
specially when serializing a Request over a PB connection (for example,
when sending it to a distrib client) so that the values survive the
transmission rather than morphing into an Unpersistable instance. This
re-merge of the branch fixes Python 2.3 compatibility.

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