# -*- test-case-name: twisted.web.test.test_web -*- # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. """This is a web-server which integrates with the twisted.internet infrastructure. """ # System Imports try: import cStringIO as StringIO except ImportError: import StringIO import base64 import string import socket import types import operator import cgi import copy import time import os from urllib import quote try: from twisted.protocols._c_urlarg import unquote except ImportError: from urllib import unquote #some useful constants NOT_DONE_YET = 1 # Twisted Imports from twisted.spread import pb from twisted.internet import reactor, protocol, defer from twisted.web import http from twisted.python import log, reflect, roots, failure, components from twisted import copyright from twisted.cred import util from twisted.persisted import styles # Sibling Imports import error, resource from twisted.web import util as webutil # backwards compatability date_time_string = http.datetimeToString string_date_time = http.stringToDatetime # Support for other methods may be implemented on a per-resource basis. supportedMethods = ('GET', 'HEAD', 'POST') class UnsupportedMethod(Exception): """Raised by a resource when faced with a strange request method. RFC 2616 (HTTP 1.1) gives us two choices when faced with this situtation: If the type of request is known to us, but not allowed for the requested resource, respond with NOT_ALLOWED. Otherwise, if the request is something we don't know how to deal with in any case, respond with NOT_IMPLEMENTED. When this exception is raised by a Resource's render method, the server will make the appropriate response. This exception's first argument MUST be a sequence of the methods the resource *does* support. """ allowedMethods = () def __init__(self, allowedMethods, *args): Exception.__init__(self, allowedMethods, *args) self.allowedMethods = allowedMethods if not operator.isSequenceType(allowedMethods): why = "but my first argument is not a sequence." s = ("First argument must be a sequence of" " supported methods, %s" % (why,)) raise TypeError, s class Request(pb.Copyable, http.Request, components.Componentized): site = None appRootURL = None __pychecker__ = 'unusednames=issuer' def __init__(self, *args, **kw): http.Request.__init__(self, *args, **kw) components.Componentized.__init__(self) self.notifications = [] def getStateToCopyFor(self, issuer): x = self.__dict__.copy() del x['transport'] # XXX refactor this attribute out; it's from protocol # del x['server'] del x['channel'] del x['content'] del x['site'] self.content.seek(0, 0) x['content_data'] = self.content.read() x['remote'] = pb.ViewPoint(issuer, self) # Address objects aren't jellyable x['host'] = tuple(x['host']) x['client'] = tuple(x['client']) return x # HTML generation helpers def sibLink(self, name): "Return the text that links to a sibling of the requested resource." if self.postpath: return (len(self.postpath)*"../") + name else: return name def childLink(self, name): "Return the text that links to a child of the requested resource." lpp = len(self.postpath) if lpp > 1: return ((lpp-1)*"../") + name elif lpp == 1: return name else: # lpp == 0 if len(self.prepath) and self.prepath[-1]: return self.prepath[-1] + '/' + name else: return name def process(self): "Process a request." # get site from channel self.site = self.channel.site # set various default headers self.setHeader('server', version) self.setHeader('date', http.datetimeToString()) self.setHeader('content-type', "text/html") # Resource Identification self.prepath = [] self.postpath = map(unquote, string.split(self.path[1:], '/')) try: resrc = self.site.getResourceFor(self) self.render(resrc) except: self.processingFailed(failure.Failure()) def render(self, resrc): try: body = resrc.render(self) except UnsupportedMethod, e: allowedMethods = e.allowedMethods if (self.method == "HEAD") and ("GET" in allowedMethods): # We must support HEAD (RFC 2616, 5.1.1). If the # resource doesn't, fake it by giving the resource # a 'GET' request and then return only the headers, # not the body. log.msg("Using GET to fake a HEAD request for %s" % (resrc,)) self.method = "GET" body = resrc.render(self) if body is NOT_DONE_YET: log.msg("Tried to fake a HEAD request for %s, but " "it got away from me." % resrc) # Oh well, I guess we won't include the content length. else: self.setHeader('content-length', str(len(body))) self.write('') self.finish() return if self.method in (supportedMethods): # We MUST include an Allow header # (RFC 2616, 10.4.6 and 14.7) self.setHeader('Allow', allowedMethods) s = ('''Your browser approached me (at %(URI)s) with''' ''' the method "%(method)s". I only allow''' ''' the method%(plural)s %(allowed)s here.''' % { 'URI': self.uri, 'method': self.method, 'plural': ((len(allowedMethods) > 1) and 's') or '', 'allowed': string.join(allowedMethods, ', ') }) epage = error.ErrorPage(http.NOT_ALLOWED, "Method Not Allowed", s) body = epage.render(self) else: epage = error.ErrorPage(http.NOT_IMPLEMENTED, "Huh?", """I don't know how to treat a""" """ %s request.""" % (self.method)) body = epage.render(self) # end except UnsupportedMethod if body == NOT_DONE_YET: return if type(body) is not types.StringType: body = error.ErrorPage(http.INTERNAL_SERVER_ERROR, "Request did not return a string", "Request: "+html.PRE(reflect.safe_repr(self))+"
"+ "Resource: "+html.PRE(reflect.safe_repr(resrc))+"
"+ "Value: "+html.PRE(reflect.safe_repr(body))).render(self) if self.method == "HEAD": if len(body) > 0: # This is a Bad Thing (RFC 2616, 9.4) log.msg("Warning: HEAD request %s for resource %s is" " returning a message body." " I think I'll eat it." % (self, resrc)) self.setHeader('content-length', str(len(body))) self.write('') else: self.setHeader('content-length', str(len(body))) self.write(body) self.finish() def processingFailed(self, reason): log.err(reason) if self.site.displayTracebacks: body = ("web.Server Traceback (most recent call last)" "web.Server Traceback (most recent call last):\n\n" "%s\n\n\n" % webutil.formatFailure(reason)) else: body = ("Processing Failed" "Processing Failed") self.setResponseCode(http.INTERNAL_SERVER_ERROR) self.setHeader('content-type',"text/html") self.setHeader('content-length', str(len(body))) self.write(body) self.finish() return reason def notifyFinish(self): """Notify when finishing the request @return: A deferred. The deferred will be triggered when the request is finished -- with a C{None} value if the request finishes successfully or with an error if the request is stopped by the client. """ self.notifications.append(defer.Deferred()) return self.notifications[-1] def connectionLost(self, reason): for d in self.notifications: d.errback(reason) self.notifications = [] def finish(self): http.Request.finish(self) for d in self.notifications: d.callback(None) self.notifications = [] def view_write(self, issuer, data): """Remote version of write; same interface. """ self.write(data) def view_finish(self, issuer): """Remote version of finish; same interface. """ self.finish() def view_addCookie(self, issuer, k, v, **kwargs): """Remote version of addCookie; same interface. """ self.addCookie(k, v, **kwargs) def view_setHeader(self, issuer, k, v): """Remote version of setHeader; same interface. """ self.setHeader(k, v) def view_setLastModified(self, issuer, when): """Remote version of setLastModified; same interface. """ self.setLastModified(when) def view_setETag(self, issuer, tag): """Remote version of setETag; same interface. """ self.setETag(tag) def view_setResponseCode(self, issuer, code): """Remote version of setResponseCode; same interface. """ self.setResponseCode(code) def view_registerProducer(self, issuer, producer, streaming): """Remote version of registerProducer; same interface. (requires a remote producer.) """ self.registerProducer(_RemoteProducerWrapper(producer), streaming) def view_unregisterProducer(self, issuer): self.unregisterProducer() ### these calls remain local session = None def getSession(self, sessionInterface = None): # Session management if not self.session: cookiename = string.join(['TWISTED_SESSION'] + self.sitepath, "_") sessionCookie = self.getCookie(cookiename) if sessionCookie: try: self.session = self.site.getSession(sessionCookie) except KeyError: pass # if it still hasn't been set, fix it up. if not self.session: self.session = self.site.makeSession() self.addCookie(cookiename, self.session.uid, path='/') self.session.touch() if sessionInterface: return self.session.getComponent(sessionInterface) return self.session def _prePathURL(self, prepath): port = self.getHost().port if self.isSecure(): default = 443 else: default = 80 if port == default: hostport = '' else: hostport = ':%d' % port return quote('http%s://%s%s/%s' % ( self.isSecure() and 's' or '', self.getRequestHostname(), hostport, string.join(prepath, '/')), "/:") def prePathURL(self): return self._prePathURL(self.prepath) def URLPath(self): from twisted.python import urlpath return urlpath.URLPath.fromRequest(self) def rememberRootURL(self): """ Remember the currently-processed part of the URL for later recalling. """ url = self._prePathURL(self.prepath[:-1]) self.appRootURL = url def getRootURL(self): """ Get a previously-remembered URL. """ return self.appRootURL class _RemoteProducerWrapper: def __init__(self, remote): self.resumeProducing = remote.remoteMethod("resumeProducing") self.pauseProducing = remote.remoteMethod("pauseProducing") self.stopProducing = remote.remoteMethod("stopProducing") class Session(components.Componentized): """A user's session with a system. This utility class contains no functionality, but is used to represent a session. """ def __init__(self, site, uid): """Initialize a session with a unique ID for that session. """ components.Componentized.__init__(self) self.site = site self.uid = uid self.expireCallbacks = [] self.touch() self.sessionNamespaces = {} def notifyOnExpire(self, callback): """Call this callback when the session expires or logs out. """ self.expireCallbacks.append(callback) def expire(self): """Expire/logout of the session. """ #log.msg("expired session %s" % self.uid) del self.site.sessions[self.uid] for c in self.expireCallbacks: c() self.expireCallbacks = [] def touch(self): self.lastModified = time.time() def checkExpired(self): # If I haven't been touched in 15 minutes: if time.time() - self.lastModified > 900: if self.site.sessions.has_key(self.uid): self.expire() else: pass #log.msg("no session to expire: %s" % self.uid) else: #log.msg("session given the will to live for 30 more minutes") reactor.callLater(1800, self.checkExpired) # previously merely: version = "TwistedWeb/%s" % copyright.version # but customized so that we can easily tell if another eXe server is running [and not just another TwistedWeb app :-)], to: version = "eXeTwistedWeb/%s" % copyright.version class Site(http.HTTPFactory): counter = 0 requestFactory = Request displayTracebacks = True def __init__(self, resource, logPath=None, timeout=60*60*12): """Initialize. """ http.HTTPFactory.__init__(self, logPath=logPath, timeout=timeout) self.sessions = {} self.resource = resource def _openLogFile(self, path): from twisted.python import logfile return logfile.LogFile(os.path.basename(path), os.path.dirname(path)) def __getstate__(self): d = self.__dict__.copy() d['sessions'] = {} return d def _mkuid(self): """(internal) Generate an opaque, unique ID for a user's session. """ import md5, random self.counter = self.counter + 1 return md5.new("%s_%s" % (str(random.random()) , str(self.counter))).hexdigest() def makeSession(self): """Generate a new Session instance, and store it for future reference. """ uid = self._mkuid() s = Session(self, uid) session = self.sessions[uid] = s reactor.callLater(1800, s.checkExpired) return session def getSession(self, uid): """Get a previously generated session, by its unique ID. This raises a KeyError if the session is not found. """ return self.sessions[uid] def buildProtocol(self, addr): """Generate a channel attached to this site. """ channel = http.HTTPFactory.buildProtocol(self, addr) channel.requestFactory = self.requestFactory channel.site = self return channel isLeaf = 0 def render(self, request): """Redirect because a Site is always a directory. """ request.redirect(request.prePathURL() + '/') request.finish() def getChildWithDefault(self, pathEl, request): """Emulate a resource's getChild method. """ request.site = self return self.resource.getChildWithDefault(pathEl, request) def getResourceFor(self, request): """Get a resource for a request. This iterates through the resource heirarchy, calling getChildWithDefault on each resource it finds for a path element, stopping when it hits an element where isLeaf is true. """ request.site = self # Sitepath is used to determine cookie names between distributed # servers and disconnected sites. request.sitepath = copy.copy(request.prepath) return resource.getChildForRequest(self.resource, request) import html