| 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 |