~landscape/zope3/newer-from-ztk

« back to all changes in this revision

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