diff plugins/cherrypy/_cphttptools.py @ 0:4385a7d0efd1 grumpy-goblin

Deleted and repushed it with the 'grumpy-goblin' branch. I forgot a y
author sirebral
date Tue, 14 Jul 2009 16:41:58 -0500
parents
children 78407d627cba
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/cherrypy/_cphttptools.py	Tue Jul 14 16:41:58 2009 -0500
@@ -0,0 +1,543 @@
+"""
+Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the CherryPy Team nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+import cpg, urllib, sys, time, traceback, types, StringIO, cgi, os
+import mimetypes, hashlib, random, string, _cputil, cperror, Cookie, urlparse
+from lib.filter import basefilter
+
+"""
+Common Service Code for CherryPy
+"""
+
+mimetypes.types_map['.dwg']='image/x-dwg'
+mimetypes.types_map['.ico']='image/x-icon'
+
+weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+
+class IndexRedirect(Exception): pass
+
+def parseFirstLine(data):
+    cpg.request.path = data.split()[1]
+    cpg.request.queryString = ""
+    cpg.request.browserUrl = cpg.request.path
+    cpg.request.paramMap = {}
+    cpg.request.paramList = [] # Only used for Xml-Rpc
+    cpg.request.filenameMap = {}
+    cpg.request.fileTypeMap = {}
+    i = cpg.request.path.find('?')
+    if i != -1:
+        # Parse parameters from URL
+        if cpg.request.path[i+1:]:
+            k = cpg.request.path[i+1:].find('?')
+            if k != -1:
+                j = cpg.request.path[:k].rfind('=')
+                if j != -1:
+                    cpg.request.path = cpg.request.path[:j+1] + \
+                        urllib.quote_plus(cpg.request.path[j+1:])
+            for paramStr in cpg.request.path[i+1:].split('&'):
+                sp = paramStr.split('=')
+                if len(sp) > 2:
+                    j = paramStr.find('=')
+                    sp = (paramStr[:j], paramStr[j+1:])
+                if len(sp) == 2:
+                    key, value = sp
+                    value = urllib.unquote_plus(value)
+                    if cpg.request.paramMap.has_key(key):
+                        # Already has a value: make a list out of it
+                        if type(cpg.request.paramMap[key]) == type([]):
+                            # Already is a list: append the new value to it
+                            cpg.request.paramMap[key].append(value)
+                        else:
+                            # Only had one value so far: start a list
+                            cpg.request.paramMap[key] = [cpg.request.paramMap[key], value]
+                    else:
+                        cpg.request.paramMap[key] = value
+        cpg.request.queryString = cpg.request.path[i+1:]
+        cpg.request.path = cpg.request.path[:i]
+
+def cookHeaders(clientAddress, remoteHost, headers, requestLine):
+    """Process the headers into the request.headerMap"""
+    cpg.request.headerMap = {}
+    cpg.request.requestLine = requestLine
+    cpg.request.simpleCookie = Cookie.SimpleCookie()
+
+    # Build headerMap
+    for item in headers.items():
+        # Warning: if there is more than one header entry for cookies (AFAIK, only Konqueror does that)
+        # only the last one will remain in headerMap (but they will be correctly stored in request.simpleCookie)
+        insertIntoHeaderMap(item[0],item[1])
+
+    # Handle cookies differently because on Konqueror, multiple cookies come on different lines with the same key
+    cookieList = headers.getallmatchingheaders('cookie')
+    for cookie in cookieList:
+        cpg.request.simpleCookie.load(cookie)
+
+    cpg.request.remoteAddr = clientAddress
+    cpg.request.remoteHost = remoteHost
+
+    # Set peer_certificate (in SSL mode) so the web app can examinate the client certificate
+    try: cpg.request.peerCertificate = self.request.get_peer_certificate()
+    except: pass
+
+    _cputil.getSpecialFunction('_cpLogMessage')("%s - %s" % (cpg.request.remoteAddr, requestLine[:-2]), "HTTP")
+
+
+def parsePostData(rfile):
+    # Read request body and put it in data
+    len = int(cpg.request.headerMap.get("Content-Length","0"))
+    if len: data = rfile.read(len)
+    else: data=""
+
+    # Put data in a StringIO so FieldStorage can read it
+    newRfile = StringIO.StringIO(data)
+    # Create a copy of headerMap with lowercase keys because
+    #   FieldStorage doesn't work otherwise
+    lowerHeaderMap = {}
+    for key, value in cpg.request.headerMap.items():
+        lowerHeaderMap[key.lower()] = value
+    forms = cgi.FieldStorage(fp = newRfile, headers = lowerHeaderMap, environ = {'REQUEST_METHOD':'POST'}, keep_blank_values = 1)
+    for key in forms.keys():
+        # Check if it's a list or not
+        valueList = forms[key]
+        if type(valueList) == type([]):
+            # It's a list of values
+            cpg.request.paramMap[key] = []
+            cpg.request.filenameMap[key] = []
+            cpg.request.fileTypeMap[key] = []
+            for item in valueList:
+                cpg.request.paramMap[key].append(item.value)
+                cpg.request.filenameMap[key].append(item.filename)
+                cpg.request.fileTypeMap[key].append(item.type)
+        else:
+            # It's a single value
+            # In case it's a file being uploaded, we save the filename in a map (user might need it)
+            cpg.request.paramMap[key] = valueList.value
+            cpg.request.filenameMap[key] = valueList.filename
+            cpg.request.fileTypeMap[key] = valueList.type
+
+def applyFilterList(methodName):
+    try:
+        filterList = _cputil.getSpecialFunction('_cpFilterList')
+        for filter in filterList:
+            method = getattr(filter, methodName, None)
+            if method:
+                method()
+    except basefilter.InternalRedirect:
+        # If we get an InternalRedirect, we start the filter list
+        #   from scratch. Is cpg.request.path or cpg.request.objectPath
+        #   has been modified by the hook, then a new filter list
+        #   will be applied.
+        # We use recursion so if there is an infinite loop, we'll
+        #   get the regular python "recursion limit exceeded" exception.
+        applyFilterList(methodName)
+
+
+def insertIntoHeaderMap(key,value):
+    normalizedKey = '-'.join([s.capitalize() for s in key.split('-')])
+    cpg.request.headerMap[normalizedKey] = value
+
+def initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile):
+    parseFirstLine(requestLine)
+    cookHeaders(clientAddress, remoteHost, headers, requestLine)
+
+    cpg.request.base = "http://" + cpg.request.headerMap['Host']
+    cpg.request.browserUrl = cpg.request.base + cpg.request.browserUrl
+    cpg.request.isStatic = False
+    cpg.request.parsePostData = True
+    cpg.request.rfile = rfile
+
+    # Change objectPath in filters to change the object that will get rendered
+    cpg.request.objectPath = None
+
+    applyFilterList('afterRequestHeader')
+
+    if cpg.request.method == 'POST' and cpg.request.parsePostData:
+        parsePostData(rfile)
+
+    applyFilterList('afterRequestBody')
+
+def doRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile):
+    # creates some attributes on cpg.response so filters can use them
+    cpg.response.wfile = wfile
+    cpg.response.sendResponse = 1
+    try:
+        initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile)
+    except basefilter.RequestHandled:
+        # request was already fully handled; it may be a cache hit
+        return
+
+    # Prepare response variables
+    now = time.time()
+    year, month, day, hh, mm, ss, wd, y, z = time.gmtime(now)
+    date = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (weekdayname[wd], day, monthname[month], year, hh, mm, ss)
+    cpg.response.headerMap = {
+        "protocolVersion": cpg.configOption.protocolVersion,
+        "Status": "200 OK",
+        "Content-Type": "text/html",
+        "Server": "CherryPy/" + cpg.__version__,
+        "Date": date,
+        "Set-Cookie": [],
+        "Content-Length": 0
+    }
+    cpg.response.simpleCookie = Cookie.SimpleCookie()
+
+    try:
+        handleRequest(cpg.response.wfile)
+    except:
+        # TODO: in some cases exceptions and filters are conflicting;
+        # error reporting seems to be broken in some cases. This code is
+        # a helper to check it
+        err = ""
+        exc_info_1 = sys.exc_info()[1]
+        if hasattr(exc_info_1, 'args') and len(exc_info_1.args) >= 1:
+            err = exc_info_1.args[0]
+
+        try:
+            _cputil.getSpecialFunction('_cpOnError')()
+
+            # Still save session data
+            if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
+                sessionId = cpg.response.simpleCookie[cpg.configOption.sessionCookieName].value
+                expirationTime = time.time() + cpg.configOption.sessionTimeout * 60
+                _cputil.getSpecialFunction('_cpSaveSessionData')(sessionId, cpg.request.sessionMap, expirationTime)
+
+            wfile.write('%s %s\r\n' % (cpg.response.headerMap['protocolVersion'], cpg.response.headerMap['Status']))
+
+            if (cpg.response.headerMap.has_key('Content-Length') and
+                    cpg.response.headerMap['Content-Length']==0):
+  	 	        buf = StringIO.StringIO()
+  	 	        [buf.write(x) for x in cpg.response.body]
+  	 	        buf.seek(0)
+  	 	        cpg.response.body = [buf.read()]
+  	 	        cpg.response.headerMap['Content-Length'] = len(cpg.response.body[0])
+
+            for key, valueList in cpg.response.headerMap.items():
+                if key not in ('Status', 'protocolVersion'):
+                    if type(valueList) != type([]): valueList = [valueList]
+                    for value in valueList:
+                        wfile.write('%s: %s\r\n'%(key, value))
+            wfile.write('\r\n')
+            for line in cpg.response.body:
+                wfile.write(line)
+        except:
+            bodyFile = StringIO.StringIO()
+            traceback.print_exc(file = bodyFile)
+            body = bodyFile.getvalue()
+            wfile.write('%s 200 OK\r\n' % cpg.configOption.protocolVersion)
+            wfile.write('Content-Type: text/plain\r\n')
+            wfile.write('Content-Length: %s\r\n' % len(body))
+            wfile.write('\r\n')
+            wfile.write(body)
+
+def sendResponse(wfile):
+    applyFilterList('beforeResponse')
+
+    # Set the content-length
+    if (cpg.response.headerMap.has_key('Content-Length') and
+            cpg.response.headerMap['Content-Length']==0):
+        buf = StringIO.StringIO()
+        [buf.write(x) for x in cpg.response.body]
+        buf.seek(0)
+        cpg.response.body = [buf.read()]
+        cpg.response.headerMap['Content-Length'] = len(cpg.response.body[0])
+
+    # Save session data
+    if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
+        sessionId = cpg.response.simpleCookie[cpg.configOption.sessionCookieName].value
+        expirationTime = time.time() + cpg.configOption.sessionTimeout * 60
+        _cputil.getSpecialFunction('_cpSaveSessionData')(sessionId, cpg.request.sessionMap, expirationTime)
+
+    wfile.write('%s %s\r\n' % (cpg.response.headerMap['protocolVersion'], cpg.response.headerMap['Status']))
+    for key, valueList in cpg.response.headerMap.items():
+        if key not in ('Status', 'protocolVersion'):
+            if type(valueList) != type([]): valueList = [valueList]
+            for value in valueList:
+                wfile.write('%s: %s\r\n' % (key, value))
+
+    # Send response cookies
+    cookie = cpg.response.simpleCookie.output()
+    if cookie:
+        wfile.write(cookie+'\r\n')
+    wfile.write('\r\n')
+
+    for line in cpg.response.body:
+        wfile.write(line)
+
+    # finalization hook for filter cleanup & logging purposes
+    applyFilterList('afterResponse')
+
+def handleRequest(wfile):
+    # Clean up expired sessions if needed:
+    now = time.time()
+    if cpg.configOption.sessionStorageType and cpg.configOption.sessionCleanUpDelay and cpg._lastSessionCleanUpTime + cpg.configOption.sessionCleanUpDelay * 60 <= now:
+        cpg._lastSessionCleanUpTime = now
+        _cputil.getSpecialFunction('_cpCleanUpOldSessions')()
+
+    # Save original values (in case they get modified by filters)
+    cpg.request.originalPath = cpg.request.path
+    cpg.request.originalParamMap = cpg.request.paramMap
+    cpg.request.originalParamList = cpg.request.paramList
+
+    path = cpg.request.path
+    if path.startswith('/'):
+        # Remove leading slash
+        path = path[1:]
+    if path.endswith('/'):
+        # Remove trailing slash
+        path = path[:-1]
+    path = urllib.unquote(path) # Replace quoted chars (eg %20) from url
+
+    # Handle static directories
+    for urlDir, fsDir in cpg.configOption.staticContentList:
+        if path == urlDir or path[:len(urlDir)+1]==urlDir+'/':
+
+            cpg.request.isStatic = 1
+
+            fname = fsDir + path[len(urlDir):]
+            start_url_var = cpg.request.browserUrl.find('?')
+            if start_url_var != -1: fname = fname + cpg.request.browserUrl[start_url_var:]
+            try:
+                stat = os.stat(fname)
+            except OSError:
+                raise cperror.NotFound
+            modifTime = stat.st_mtime
+
+            strModifTime = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(modifTime))
+
+            # Check if browser sent "if-modified-since" in request header
+            if cpg.request.headerMap.has_key('If-Modified-Since'):
+                # Check if if-modified-since date is the same as strModifTime
+                if cpg.request.headerMap['If-Modified-Since'] == strModifTime:
+                    cpg.response.headerMap = {
+                        'Status': 304,
+                        'protocolVersion': cpg.configOption.protocolVersion,
+                        'Date': cpg.response.headerMap['Date']}
+                    cpg.response.body = []
+                    sendResponse(wfile)
+                    return
+
+            cpg.response.headerMap['Last-Modified'] = strModifTime
+            # Set Content-Length and use an iterable (file object)
+            #   this way CP won't load the whole file in memory
+            cpg.response.headerMap['Content-Length'] = stat[6]
+            cpg.response.body = open(fname, 'rb')
+            # Set content-type based on filename extension
+            i = path.rfind('.')
+            if i != -1: ext = path[i:]
+            else: ext = ""
+            contentType = mimetypes.types_map.get(ext, "text/plain")
+            cpg.response.headerMap['Content-Type'] = contentType
+            sendResponse(wfile)
+            return
+
+    # Get session data
+    if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
+        now = time.time()
+        # First, get sessionId from cookie
+        try: sessionId = cpg.request.simpleCookie[cpg.configOption.sessionCookieName].value
+        except: sessionId=None
+        if sessionId:
+            # Load session data from wherever it was stored
+            sessionData = _cputil.getSpecialFunction('_cpLoadSessionData')(sessionId)
+            if sessionData == None:
+                sessionId = None
+            else:
+                cpg.request.sessionMap, expirationTime = sessionData
+                # Check that is hasn't expired
+                if now > expirationTime:
+                    # Session expired
+                    sessionId = None
+
+        # Create a new sessionId if needed
+        if not sessionId:
+            cpg.request.sessionMap = {}
+            sessionId = generateSessionId()
+            cpg.request.sessionMap['_sessionId'] = sessionId
+
+        cpg.response.simpleCookie[cpg.configOption.sessionCookieName] = sessionId
+        cpg.response.simpleCookie[cpg.configOption.sessionCookieName]['path'] = '/'
+        cpg.response.simpleCookie[cpg.configOption.sessionCookieName]['version'] = 1
+
+    try:
+        func, objectPathList, virtualPathList = mapPathToObject()
+    except IndexRedirect, inst:
+        # For an IndexRedirect, we don't go through the regular
+        #   mechanism: we return the redirect immediately
+        newUrl = urlparse.urljoin(cpg.request.base, inst.args[0])
+        wfile.write('%s 302\r\n' % (cpg.response.headerMap['protocolVersion']))
+        cpg.response.headerMap['Location'] = newUrl
+        for key, valueList in cpg.response.headerMap.items():
+            if key not in ('Status', 'protocolVersion'):
+                if type(valueList) != type([]): valueList = [valueList]
+                for value in valueList:
+                    wfile.write('%s: %s\r\n'%(key, value))
+        wfile.write('\r\n')
+        return
+
+    # Remove "root" from objectPathList and join it to get objectPath
+    cpg.request.objectPath = '/' + '/'.join(objectPathList[1:])
+    body = func(*(virtualPathList + cpg.request.paramList), **(cpg.request.paramMap))
+
+    # builds a uniform return type
+    if not isinstance(body, types.GeneratorType):
+        cpg.response.body = [body]
+    else:
+        cpg.response.body = body
+
+    if cpg.response.sendResponse:
+        sendResponse(wfile)
+
+def generateSessionId():
+    s = ''
+    for i in range(50):
+        s += random.choice(string.letters+string.digits)
+    s += '%s'%time.time()
+    return hashlib.hashlib(s).hexdigest()
+
+def getObjFromPath(objPathList, objCache):
+    """ For a given objectPathList (like ['root', 'a', 'b', 'index']),
+         return the object (or None if it doesn't exist).
+         Also keep a cache for maximum efficiency
+    """
+    # Let cpg be the first valid object.
+    validObjects = ["cpg"]
+
+    # Scan the objPathList in order from left to right
+    for index, obj in enumerate(objPathList):
+        # maps virtual filenames to Python identifiers (substitutes '.' for '_')
+        obj = obj.replace('.', '_')
+
+        # currentObjStr holds something like 'cpg.root.something.else'
+        currentObjStr = ".".join(validObjects)
+
+        #---------------
+        #   Cache check
+        #---------------
+        # Generate a cacheKey from the first 'index' elements of objPathList
+        cacheKey = tuple(objPathList[:index+1])
+        # Is this cacheKey in the objCache?
+        if cacheKey in objCache:
+            # And is its value not None?
+            if objCache[cacheKey]:
+                # Yes, then add it to the list of validObjects
+                validObjects.append(obj)
+                # OK, go to the next iteration
+                continue
+            # Its value is None, so we stop
+            # (This means it is not a valid object)
+            break
+
+        #-----------------
+        # Attribute check
+        #-----------------
+        if getattr(eval(currentObjStr), obj, None):
+            #  obj is a valid attribute of the current object
+            validObjects.append(obj)
+            #  Store it in the cache
+            objCache[cacheKey] = eval(".".join(validObjects))
+        else:
+            # obj is not a valid attribute
+            # Store None in the cache
+            objCache[cacheKey] = None
+            # Stop, we won't process the remaining objPathList
+            break
+
+    # Return the last cached object (even if its None)
+    return objCache[cacheKey]
+
+def mapPathToObject(path = None):
+    # Traverse path:
+    # for /a/b?arg=val, we'll try:
+    #   root.a.b.index -> redirect to /a/b/?arg=val
+    #   root.a.b.default(arg='val') -> redirect to /a/b/?arg=val
+    #   root.a.b(arg='val')
+    #   root.a.default('b', arg='val')
+    #   root.default('a', 'b', arg='val')
+
+    # Also, we ignore trailing slashes
+    # Also, a method has to have ".exposed = True" in order to be exposed
+
+    if path is None:
+        path = cpg.request.objectPath or cpg.request.path
+    if path.startswith('/'):
+        path = path[1:] # Remove leading slash
+    if path.endswith('/'):
+        path = path[:-1] # Remove trailing slash
+
+    if not path:
+        objectPathList = []
+    else:
+        objectPathList = path.split('/')
+    objectPathList = ['root'] + objectPathList + ['index']
+
+    # Try successive objects... (and also keep the remaining object list)
+    objCache = {}
+    isFirst = True
+    isSecond = False
+    isDefault = False
+    foundIt = False
+    virtualPathList = []
+    while objectPathList:
+        if isFirst or isSecond:
+            # Only try this for a.b.index() or a.b()
+            candidate = getObjFromPath(objectPathList, objCache)
+            if callable(candidate) and getattr(candidate, 'exposed', False):
+                foundIt = True
+                break
+        # Couldn't find the object: pop one from the list and try "default"
+        lastObj = objectPathList.pop()
+        if (not isFirst) or (not path):
+            virtualPathList.insert(0, lastObj)
+            objectPathList.append('default')
+            candidate = getObjFromPath(objectPathList, objCache)
+            if callable(candidate) and getattr(candidate, 'exposed', False):
+                foundIt = True
+                isDefault = True
+                break
+            objectPathList.pop() # Remove "default"
+        if isSecond:
+            isSecond = False
+        if isFirst:
+            isFirst = False
+            isSecond = True
+
+    # Check results of traversal
+    if not foundIt:
+        raise cperror.NotFound # We didn't find anything
+
+    if isFirst:
+        # We found the extra ".index"
+        # Check if the original path had a trailing slash (otherwise, do
+        #   a redirect)
+        if cpg.request.path[-1] != '/':
+            newUrl = cpg.request.path + '/'
+            if cpg.request.queryString: newUrl += cpg.request.queryString
+            raise IndexRedirect(newUrl)
+
+    return candidate, objectPathList, virtualPathList