Mercurial > traipse_dev
view plugins/cherrypy/_cphttptools.py @ 80:51bb772c3609 ornery-dev
Update Manager nearly 100% complete. Buttons on Controls panel need to be implmented. Users can now look over the revisions for each branch / revision.
author | sirebral |
---|---|
date | Mon, 24 Aug 2009 00:56:09 -0500 |
parents | 78407d627cba |
children |
line wrap: on
line source
""" 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.shal(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