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