1
# -*- test-case-name: twisted.web.test.test_web -*-
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
# See LICENSE for details.
7
"""This is a web-server which integrates with the twisted.internet
14
import cStringIO as StringIO
27
from urllib import quote
29
from twisted.protocols._c_urlarg import unquote
31
from urllib import unquote
33
#some useful constants
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
46
import error, resource
47
from twisted.web import util as webutil
50
# backwards compatability
51
date_time_string = http.datetimeToString
52
string_date_time = http.stringToDatetime
54
# Support for other methods may be implemented on a per-resource basis.
55
supportedMethods = ('GET', 'HEAD', 'POST')
58
class UnsupportedMethod(Exception):
59
"""Raised by a resource when faced with a strange request method.
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.
66
When this exception is raised by a Resource's render method, the server
67
will make the appropriate response.
69
This exception's first argument MUST be a sequence of the methods the
70
resource *does* support.
75
def __init__(self, allowedMethods, *args):
76
Exception.__init__(self, allowedMethods, *args)
77
self.allowedMethods = allowedMethods
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,))
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)
93
class Request(pb.Copyable, http.Request, components.Componentized):
97
__pychecker__ = 'unusednames=issuer'
99
def __init__(self, *args, **kw):
100
http.Request.__init__(self, *args, **kw)
101
components.Componentized.__init__(self)
102
self.notifications = []
104
def getStateToCopyFor(self, issuer):
105
x = self.__dict__.copy()
107
# XXX refactor this attribute out; it's from protocol
112
self.content.seek(0, 0)
113
x['content_data'] = self.content.read()
114
x['remote'] = pb.ViewPoint(issuer, self)
116
# Address objects aren't jellyable
117
x['host'] = _addressToTuple(x['host'])
118
x['client'] = _addressToTuple(x['client'])
122
# HTML generation helpers
124
def sibLink(self, name):
125
"Return the text that links to a sibling of the requested resource."
127
return (len(self.postpath)*"../") + name
131
def childLink(self, name):
132
"Return the text that links to a child of the requested resource."
133
lpp = len(self.postpath)
135
return ((lpp-1)*"../") + name
139
if len(self.prepath) and self.prepath[-1]:
140
return self.prepath[-1] + '/' + name
147
# get site from channel
148
self.site = self.channel.site
150
# set various default headers
151
self.setHeader('server', version)
152
self.setHeader('date', http.datetimeToString())
153
self.setHeader('content-type', "text/html")
155
# Resource Identification
157
self.postpath = map(unquote, string.split(self.path[1:], '/'))
159
resrc = self.site.getResourceFor(self)
162
self.processingFailed(failure.Failure())
165
def render(self, resrc):
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,
175
log.msg("Using GET to fake a HEAD request for %s" %
178
body = resrc.render(self)
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.
185
self.setHeader('content-length', str(len(body)))
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.''' % {
199
'method': self.method,
200
'plural': ((len(allowedMethods) > 1) and 's') or '',
201
'allowed': string.join(allowedMethods, ', ')
203
epage = error.ErrorPage(http.NOT_ALLOWED,
204
"Method Not Allowed", s)
205
body = epage.render(self)
207
epage = error.ErrorPage(http.NOT_IMPLEMENTED, "Huh?",
208
"""I don't know how to treat a"""
211
body = epage.render(self)
212
# end except UnsupportedMethod
214
if body == NOT_DONE_YET:
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)
223
if self.method == "HEAD":
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."
230
self.setHeader('content-length', str(len(body)))
233
self.setHeader('content-length', str(len(body)))
237
def processingFailed(self, 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))
245
body = ("<html><head><title>Processing Failed</title></head><body>"
246
"<b>Processing Failed</b></body></html>")
248
self.setResponseCode(http.INTERNAL_SERVER_ERROR)
249
self.setHeader('content-type',"text/html")
250
self.setHeader('content-length', str(len(body)))
255
def notifyFinish(self):
256
"""Notify when finishing the request
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
263
self.notifications.append(defer.Deferred())
264
return self.notifications[-1]
266
def connectionLost(self, reason):
267
for d in self.notifications:
269
self.notifications = []
272
http.Request.finish(self)
273
for d in self.notifications:
275
self.notifications = []
277
def view_write(self, issuer, data):
278
"""Remote version of write; same interface.
282
def view_finish(self, issuer):
283
"""Remote version of finish; same interface.
287
def view_addCookie(self, issuer, k, v, **kwargs):
288
"""Remote version of addCookie; same interface.
290
self.addCookie(k, v, **kwargs)
292
def view_setHeader(self, issuer, k, v):
293
"""Remote version of setHeader; same interface.
297
def view_setLastModified(self, issuer, when):
298
"""Remote version of setLastModified; same interface.
300
self.setLastModified(when)
302
def view_setETag(self, issuer, tag):
303
"""Remote version of setETag; same interface.
307
def view_setResponseCode(self, issuer, code):
308
"""Remote version of setResponseCode; same interface.
310
self.setResponseCode(code)
312
def view_registerProducer(self, issuer, producer, streaming):
313
"""Remote version of registerProducer; same interface.
314
(requires a remote producer.)
316
self.registerProducer(_RemoteProducerWrapper(producer), streaming)
318
def view_unregisterProducer(self, issuer):
319
self.unregisterProducer()
321
### these calls remain local
325
def getSession(self, sessionInterface = None):
328
cookiename = string.join(['TWISTED_SESSION'] + self.sitepath, "_")
329
sessionCookie = self.getCookie(cookiename)
332
self.session = self.site.getSession(sessionCookie)
335
# if it still hasn't been set, fix it up.
337
self.session = self.site.makeSession()
338
self.addCookie(cookiename, self.session.uid, path='/')
341
return self.session.getComponent(sessionInterface)
344
def _prePathURL(self, prepath):
345
port = self.getHost().port
353
hostport = ':%d' % port
354
return quote('http%s://%s%s/%s' % (
355
self.isSecure() and 's' or '',
356
self.getRequestHostname(),
358
string.join(prepath, '/')), "/:")
360
def prePathURL(self):
361
return self._prePathURL(self.prepath)
364
from twisted.python import urlpath
365
return urlpath.URLPath.fromRequest(self)
367
def rememberRootURL(self):
369
Remember the currently-processed part of the URL for later
372
url = self._prePathURL(self.prepath[:-1])
373
self.appRootURL = url
375
def getRootURL(self):
377
Get a previously-remembered URL.
379
return self.appRootURL
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")
389
class Session(components.Componentized):
390
"""A user's session with a system.
392
This utility class contains no functionality, but is used to
395
def __init__(self, site, uid):
396
"""Initialize a session with a unique ID for that session.
398
components.Componentized.__init__(self)
401
self.expireCallbacks = []
402
self.checkExpiredLoop = task.LoopingCall(self.checkExpired)
404
self.sessionNamespaces = {}
406
def notifyOnExpire(self, callback):
407
"""Call this callback when the session expires or logs out.
409
self.expireCallbacks.append(callback)
412
"""Expire/logout of the session.
414
#log.msg("expired session %s" % self.uid)
415
del self.site.sessions[self.uid]
416
for c in self.expireCallbacks:
418
self.expireCallbacks = []
419
self.checkExpiredLoop.stop()
420
# Break reference cycle.
421
self.checkExpiredLoop = None
424
self.lastModified = time.time()
426
def checkExpired(self):
427
"""Is it time for me to expire?
429
If I haven't been touched in fifteen minutes, I will call my
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):
439
version = "TwistedWeb/%s" % copyright.version
442
class Site(http.HTTPFactory):
445
requestFactory = Request
446
displayTracebacks = True
448
def __init__(self, resource, logPath=None, timeout=60*60*12):
451
http.HTTPFactory.__init__(self, logPath=logPath, timeout=timeout)
453
self.resource = resource
455
def _openLogFile(self, path):
456
from twisted.python import logfile
457
return logfile.LogFile(os.path.basename(path), os.path.dirname(path))
459
def __getstate__(self):
460
d = self.__dict__.copy()
465
"""(internal) Generate an opaque, unique ID for a user's session.
468
self.counter = self.counter + 1
469
return md5.new("%s_%s" % (str(random.random()) , str(self.counter))).hexdigest()
471
def makeSession(self):
472
"""Generate a new Session instance, and store it for future reference.
475
session = self.sessions[uid] = Session(self, uid)
476
session.checkExpiredLoop.start(1800)
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.
483
return self.sessions[uid]
485
def buildProtocol(self, addr):
486
"""Generate a channel attached to this site.
488
channel = http.HTTPFactory.buildProtocol(self, addr)
489
channel.requestFactory = self.requestFactory
495
def render(self, request):
496
"""Redirect because a Site is always a directory.
498
request.redirect(request.prePathURL() + '/')
501
def getChildWithDefault(self, pathEl, request):
502
"""Emulate a resource's getChild method.
505
return self.resource.getChildWithDefault(pathEl, request)
507
def getResourceFor(self, request):
508
"""Get a resource for a request.
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.
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)