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