1# -*- test-case-name: nevow.test.test_appserver -*- 2# Copyright (c) 2004-2008 Divmod. 3# See LICENSE for details. 4 5""" 6A web application server built using twisted.web 7""" 8 9import cgi 10import warnings 11from collections import MutableMapping 12from urllib import unquote 13 14from zope.interface import implements, classImplements 15 16import twisted.python.components as tpc 17from twisted.web import server 18 19try: 20 from twisted.web import http 21except ImportError: 22 from twisted.protocols import http 23 24from twisted.python import log 25from twisted.internet import defer 26 27from nevow import context 28from nevow import inevow 29from nevow import url 30from nevow import flat 31from nevow import stan 32 33 34 35class _DictHeaders(MutableMapping): 36 """ 37 A C{dict}-like wrapper around L{Headers} to provide backwards compatibility 38 for L{twisted.web.http.Request.received_headers} and 39 L{twisted.web.http.Request.headers} which used to be plain C{dict} 40 instances. 41 42 @type _headers: L{Headers} 43 @ivar _headers: The real header storage object. 44 """ 45 def __init__(self, headers): 46 self._headers = headers 47 48 49 def __getitem__(self, key): 50 """ 51 Return the last value for header of C{key}. 52 """ 53 if self._headers.hasHeader(key): 54 return self._headers.getRawHeaders(key)[-1] 55 raise KeyError(key) 56 57 58 def __setitem__(self, key, value): 59 """ 60 Set the given header. 61 """ 62 self._headers.setRawHeaders(key, [value]) 63 64 65 def __delitem__(self, key): 66 """ 67 Delete the given header. 68 """ 69 if self._headers.hasHeader(key): 70 self._headers.removeHeader(key) 71 else: 72 raise KeyError(key) 73 74 75 def __iter__(self): 76 """ 77 Return an iterator of the lowercase name of each header present. 78 """ 79 for k, v in self._headers.getAllRawHeaders(): 80 yield k.lower() 81 82 83 def __len__(self): 84 """ 85 Return the number of distinct headers present. 86 """ 87 # XXX Too many _ 88 return len(self._headers._rawHeaders) 89 90 91 # Extra methods that MutableMapping doesn't care about but that we do. 92 def copy(self): 93 """ 94 Return a C{dict} mapping each header name to the last corresponding 95 header value. 96 """ 97 return dict(self.items()) 98 99 100 def has_key(self, key): 101 """ 102 Return C{True} if C{key} is a header in this collection, C{False} 103 otherwise. 104 """ 105 return key in self 106 107 108 109class UninformativeExceptionHandler: 110 implements(inevow.ICanHandleException) 111 112 def renderHTTP_exception(self, ctx, reason): 113 request = inevow.IRequest(ctx) 114 log.err(reason) 115 request.write("<html><head><title>Internal Server Error</title></head>") 116 request.write("<body><h1>Internal Server Error</h1>An error occurred rendering the requested page. To see a more detailed error message, enable tracebacks in the configuration.</body></html>") 117 118 request.finishRequest( False ) 119 120 def renderInlineException(self, context, reason): 121 log.err(reason) 122 return """<div style="border: 1px dashed red; color: red; clear: both">[[ERROR]]</div>""" 123 124 125class DefaultExceptionHandler: 126 implements(inevow.ICanHandleException) 127 128 def renderHTTP_exception(self, ctx, reason): 129 log.err(reason) 130 request = inevow.IRequest(ctx) 131 request.setResponseCode(http.INTERNAL_SERVER_ERROR) 132 request.write("<html><head><title>Exception</title></head><body>") 133 from nevow import failure 134 result = failure.formatFailure(reason) 135 request.write(''.join(flat.flatten(result))) 136 request.write("</body></html>") 137 138 request.finishRequest( False ) 139 140 def renderInlineException(self, context, reason): 141 from nevow import failure 142 formatted = failure.formatFailure(reason) 143 desc = str(reason) 144 return flat.serialize([ 145 stan.xml("""<div style="border: 1px dashed red; color: red; clear: both" onclick="this.childNodes[1].style.display = this.childNodes[1].style.display == 'none' ? 'block': 'none'">"""), 146 desc, 147 stan.xml('<div style="display: none">'), 148 formatted, 149 stan.xml('</div></div>') 150 ], context) 151 152 153errorMarker = object() 154 155 156def processingFailed(reason, request, ctx): 157 try: 158 handler = inevow.ICanHandleException(ctx) 159 handler.renderHTTP_exception(ctx, reason) 160 except: 161 request.setResponseCode(http.INTERNAL_SERVER_ERROR) 162 log.msg("Exception rendering error page:", isErr=1) 163 log.err() 164 log.err("Original exception:", isErr=1) 165 log.err(reason) 166 request.write("<html><head><title>Internal Server Error</title></head>") 167 request.write("<body><h1>Internal Server Error</h1>An error occurred rendering the requested page. Additionally, an error occurred rendering the error page.</body></html>") 168 request.finishRequest( False ) 169 170 return errorMarker 171 172 173def defaultExceptionHandlerFactory(ctx): 174 return DefaultExceptionHandler() 175 176 177class NevowRequest(tpc.Componentized, server.Request): 178 """ 179 A Request subclass which does additional 180 processing if a form was POSTed. When a form is POSTed, 181 we create a cgi.FieldStorage instance using the data posted, 182 and set it as the request.fields attribute. This way, we can 183 get at information about filenames and mime-types of 184 files that were posted. 185 186 TODO: cgi.FieldStorage blocks while decoding the MIME. 187 Rewrite it to do the work in chunks, yielding from time to 188 time. 189 190 @ivar fields: C{None} or, if the HTTP method is B{POST}, a 191 L{cgi.FieldStorage} instance giving the content of the POST. 192 193 @ivar _lostConnection: A flag which keeps track of whether the response to 194 this request has been interrupted (for example, by the connection being 195 lost) or not. C{False} until this happens, C{True} afterwards. 196 @type _lostConnection: L{bool} 197 """ 198 implements(inevow.IRequest) 199 200 fields = None 201 _lostConnection = False 202 203 def __init__(self, *args, **kw): 204 server.Request.__init__(self, *args, **kw) 205 tpc.Componentized.__init__(self) 206 207 self.notifyFinish().addErrback(self._flagLostConnection) 208 209 210 def _flagLostConnection(self, error): 211 """ 212 Observe and record an error trying to deliver the response for this 213 request. 214 """ 215 self._lostConnection = True 216 217 218 def process(self): 219 # extra request parsing 220 if self.method == 'POST': 221 t = self.content.tell() 222 self.content.seek(0) 223 self.fields = cgi.FieldStorage( 224 self.content, _DictHeaders(self.requestHeaders), 225 environ={'REQUEST_METHOD': 'POST'}) 226 self.content.seek(t) 227 228 # get site from channel 229 self.site = self.channel.site 230 231 # set various default headers 232 self.setHeader('server', server.version) 233 self.setHeader('date', server.http.datetimeToString()) 234 self.setHeader('content-type', "text/html; charset=UTF-8") 235 236 # Resource Identification 237 self.prepath = [] 238 self.postpath = map(unquote, self.path[1:].split('/')) 239 self.sitepath = [] 240 241 self.deferred = defer.Deferred() 242 243 requestContext = context.RequestContext(parent=self.site.context, tag=self) 244 requestContext.remember( (), inevow.ICurrentSegments) 245 requestContext.remember(tuple(self.postpath), inevow.IRemainingSegments) 246 247 return self.site.getPageContextForRequestContext( 248 requestContext 249 ).addErrback( 250 processingFailed, self, requestContext 251 ).addCallback( 252 self.gotPageContext 253 ) 254 255 def gotPageContext(self, pageContext): 256 if pageContext is not errorMarker: 257 return defer.maybeDeferred( 258 pageContext.tag.renderHTTP, pageContext 259 ).addBoth( 260 self._cbSetLogger, pageContext 261 ).addErrback( 262 processingFailed, self, pageContext 263 ).addCallback( 264 self._cbFinishRender, pageContext 265 ) 266 267 def finish(self): 268 self.deferred.callback("") 269 270 271 def finishRequest(self, success): 272 """ 273 Indicate the response to this request has been completely generated 274 (headers have been set, the response body has been completely written). 275 276 @param success: Indicate whether this response is considered successful 277 or not. Not used. 278 """ 279 if not self._lostConnection: 280 # Only bother doing the work associated with finishing if the 281 # connection is still there. 282 server.Request.finish(self) 283 284 285 def _cbFinishRender(self, html, ctx): 286 """ 287 Callback for the page rendering process having completed. 288 289 @param html: Either the content of the response body (L{bytes}) or a 290 marker that an exception occurred and has already been handled or 291 an object adaptable to L{IResource} to use to render the response. 292 """ 293 if self._lostConnection: 294 # No response can be sent at this point. 295 pass 296 elif isinstance(html, str): 297 self.write(html) 298 self.finishRequest( True ) 299 elif html is errorMarker: 300 ## Error webpage has already been rendered and finish called 301 pass 302 else: 303 res = inevow.IResource(html) 304 pageContext = context.PageContext(tag=res, parent=ctx) 305 return self.gotPageContext(pageContext) 306 return html 307 308 _logger = None 309 def _cbSetLogger(self, result, ctx): 310 try: 311 logger = ctx.locate(inevow.ILogger) 312 except KeyError: 313 pass 314 else: 315 self._logger = lambda : logger.log(ctx) 316 317 return result 318 319 session = None 320 321 def getSession(self, sessionInterface=None): 322 if self.session is not None: 323 self.session.touch() 324 if sessionInterface: 325 return sessionInterface(self.session) 326 return self.session 327 ## temporary until things settle down with the new sessions 328 return server.Request.getSession(self, sessionInterface) 329 330 def URLPath(self): 331 return url.URL.fromContext(self) 332 333 def rememberRootURL(self, url=None): 334 """ 335 Remember the currently-processed part of the URL for later 336 recalling. 337 """ 338 if url is None: 339 return server.Request.rememberRootURL(self) 340 else: 341 self.appRootURL = url 342 343 344 def _warnHeaders(self, old, new): 345 """ 346 Emit a warning related to use of one of the deprecated C{headers} or 347 C{received_headers} attributes. 348 349 @param old: The name of the deprecated attribute to which the warning 350 pertains. 351 352 @param new: The name of the preferred attribute which replaces the old 353 attribute. 354 """ 355 warnings.warn( 356 category=DeprecationWarning, 357 message=( 358 "nevow.appserver.NevowRequest.%(old)s was deprecated in " 359 "Nevow 0.13.0: Please use nevow.appserver.NevowRequest." 360 "%(new)s instead." % dict(old=old, new=new)), 361 stacklevel=3) 362 363 364 @property 365 def headers(self): 366 """ 367 Transform the L{Headers}-style C{responseHeaders} attribute into a 368 deprecated C{dict}-style C{headers} attribute. 369 """ 370 self._warnHeaders("headers", "responseHeaders") 371 return _DictHeaders(self.responseHeaders) 372 373 374 @property 375 def received_headers(self): 376 """ 377 Transform the L{Headers}-style C{requestHeaders} attribute into a 378 deprecated C{dict}-style C{received_headers} attribute. 379 """ 380 self._warnHeaders("received_headers", "requestHeaders") 381 return _DictHeaders(self.requestHeaders) 382 383 384def sessionFactory(ctx): 385 """Given a RequestContext instance with a Request as .tag, return a session 386 """ 387 return ctx.tag.getSession() 388 389requestFactory = lambda ctx: ctx.tag 390 391 392class NevowSite(server.Site): 393 requestFactory = NevowRequest 394 395 def __init__(self, resource, *args, **kwargs): 396 resource.addSlash = True 397 server.Site.__init__(self, resource, *args, **kwargs) 398 self.context = context.SiteContext() 399 400 def remember(self, obj, inter=None): 401 """Remember the given object for the given interfaces (or all interfaces 402 obj implements) in the site's context. 403 404 The site context is the parent of all other contexts. Anything 405 remembered here will be available throughout the site. 406 """ 407 self.context.remember(obj, inter) 408 409 def getPageContextForRequestContext(self, ctx): 410 """Retrieve a resource from this site for a particular request. The 411 resource will be wrapped in a PageContext which keeps track 412 of how the resource was located. 413 """ 414 path = inevow.IRemainingSegments(ctx) 415 res = inevow.IResource(self.resource) 416 pageContext = context.PageContext(tag=res, parent=ctx) 417 return defer.maybeDeferred(res.locateChild, pageContext, path).addCallback( 418 self.handleSegment, ctx.tag, path, pageContext 419 ) 420 421 def handleSegment(self, result, request, path, pageContext): 422 if result is errorMarker: 423 return errorMarker 424 425 newres, newpath = result 426 # If the child resource is None then display a 404 page 427 if newres is None: 428 from nevow.rend import FourOhFour 429 return context.PageContext(tag=FourOhFour(), parent=pageContext) 430 431 # If we got a deferred then we need to call back later, once the 432 # child is actually available. 433 if isinstance(newres, defer.Deferred): 434 return newres.addCallback( 435 lambda actualRes: self.handleSegment( 436 (actualRes, newpath), request, path, pageContext)) 437 438 439 # 440 # FIX A GIANT LEAK. Is this code really useful anyway? 441 # 442 newres = inevow.IResource(newres)#, persist=True) 443 if newres is pageContext.tag: 444 assert not newpath is path, "URL traversal cycle detected when attempting to locateChild %r from resource %r." % (path, pageContext.tag) 445 assert len(newpath) < len(path), "Infinite loop impending..." 446 447 ## We found a Resource... update the request.prepath and postpath 448 for x in xrange(len(path) - len(newpath)): 449 if request.postpath: 450 request.prepath.append(request.postpath.pop(0)) 451 452 ## Create a context object to represent this new resource 453 ctx = context.PageContext(tag=newres, parent=pageContext) 454 ctx.remember(tuple(request.prepath), inevow.ICurrentSegments) 455 ctx.remember(tuple(request.postpath), inevow.IRemainingSegments) 456 457 res = newres 458 path = newpath 459 460 if not path: 461 return ctx 462 463 return defer.maybeDeferred( 464 res.locateChild, ctx, path 465 ).addErrback( 466 processingFailed, request, ctx 467 ).addCallback( 468 self.handleSegment, request, path, ctx 469 ) 470 471 def log(self, request): 472 if request._logger is None: 473 server.Site.log(self, request) 474 else: 475 request._logger() 476 477 478## This should be moved somewhere else, it's cluttering up this module. 479 480class OldResourceAdapter(object): 481 implements(inevow.IResource) 482 483 # This is required to properly handle the interaction between 484 # original.isLeaf and request.postpath, from which PATH_INFO is set in 485 # twcgi. Because we have no choice but to consume all elements in 486 # locateChild to terminate the recursion, we do so, but first save the 487 # length of prepath in real_prepath_len. Subsequently in renderHTTP, if 488 # real_prepath_len is not None, prepath is correct to the saved length and 489 # the extra segments moved to postpath. If real_prepath_len is None, then 490 # locateChild has never been called, so we know not the real length, so we 491 # do nothing, which is correct. 492 real_prepath_len = None 493 494 def __init__(self, original): 495 self.original = original 496 497 def __repr__(self): 498 return "<%s @ 0x%x adapting %r>" % (self.__class__.__name__, id(self), self.original) 499 500 def locateChild(self, ctx, segments): 501 request = inevow.IRequest(ctx) 502 if self.original.isLeaf: 503 self.real_prepath_len = len(request.prepath) 504 return self, () 505 name = segments[0] 506 request.prepath.append(request.postpath.pop(0)) 507 res = self.original.getChildWithDefault(name, request) 508 request.postpath.insert(0, request.prepath.pop()) 509 if isinstance(res, defer.Deferred): 510 return res.addCallback(lambda res: (res, segments[1:])) 511 return res, segments[1:] 512 513 def _handle_NOT_DONE_YET(self, data, request): 514 if data == server.NOT_DONE_YET: 515 return request.deferred 516 else: 517 return data 518 519 def renderHTTP(self, ctx): 520 request = inevow.IRequest(ctx) 521 if self.real_prepath_len is not None: 522 request.postpath = request.prepath[self.real_prepath_len:] 523 del request.prepath[self.real_prepath_len:] 524 result = defer.maybeDeferred(self.original.render, request).addCallback( 525 self._handle_NOT_DONE_YET, request) 526 return result 527 528 def willHandle_notFound(self, request): 529 if hasattr(self.original, 'willHandle_notFound'): 530 return self.original.willHandle_notFound(request) 531 return False 532 533 def renderHTTP_notFound(self, ctx): 534 return self.original.renderHTTP_notFound(ctx) 535 536 537from nevow import rend 538 539NotFound = rend.NotFound 540FourOhFour = rend.FourOhFour 541 542classImplements(server.Session, inevow.ISession) 543