comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:4385a7d0efd1
1 """
2 Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
7
8 * Redistributions of source code must retain the above copyright notice,
9 this list of conditions and the following disclaimer.
10 * Redistributions in binary form must reproduce the above copyright notice,
11 this list of conditions and the following disclaimer in the documentation
12 and/or other materials provided with the distribution.
13 * Neither the name of the CherryPy Team nor the names of its contributors
14 may be used to endorse or promote products derived from this software
15 without specific prior written permission.
16
17 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
21 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 """
28
29 import cpg, urllib, sys, time, traceback, types, StringIO, cgi, os
30 import mimetypes, hashlib, random, string, _cputil, cperror, Cookie, urlparse
31 from lib.filter import basefilter
32
33 """
34 Common Service Code for CherryPy
35 """
36
37 mimetypes.types_map['.dwg']='image/x-dwg'
38 mimetypes.types_map['.ico']='image/x-icon'
39
40 weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
41 monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
42
43 class IndexRedirect(Exception): pass
44
45 def parseFirstLine(data):
46 cpg.request.path = data.split()[1]
47 cpg.request.queryString = ""
48 cpg.request.browserUrl = cpg.request.path
49 cpg.request.paramMap = {}
50 cpg.request.paramList = [] # Only used for Xml-Rpc
51 cpg.request.filenameMap = {}
52 cpg.request.fileTypeMap = {}
53 i = cpg.request.path.find('?')
54 if i != -1:
55 # Parse parameters from URL
56 if cpg.request.path[i+1:]:
57 k = cpg.request.path[i+1:].find('?')
58 if k != -1:
59 j = cpg.request.path[:k].rfind('=')
60 if j != -1:
61 cpg.request.path = cpg.request.path[:j+1] + \
62 urllib.quote_plus(cpg.request.path[j+1:])
63 for paramStr in cpg.request.path[i+1:].split('&'):
64 sp = paramStr.split('=')
65 if len(sp) > 2:
66 j = paramStr.find('=')
67 sp = (paramStr[:j], paramStr[j+1:])
68 if len(sp) == 2:
69 key, value = sp
70 value = urllib.unquote_plus(value)
71 if cpg.request.paramMap.has_key(key):
72 # Already has a value: make a list out of it
73 if type(cpg.request.paramMap[key]) == type([]):
74 # Already is a list: append the new value to it
75 cpg.request.paramMap[key].append(value)
76 else:
77 # Only had one value so far: start a list
78 cpg.request.paramMap[key] = [cpg.request.paramMap[key], value]
79 else:
80 cpg.request.paramMap[key] = value
81 cpg.request.queryString = cpg.request.path[i+1:]
82 cpg.request.path = cpg.request.path[:i]
83
84 def cookHeaders(clientAddress, remoteHost, headers, requestLine):
85 """Process the headers into the request.headerMap"""
86 cpg.request.headerMap = {}
87 cpg.request.requestLine = requestLine
88 cpg.request.simpleCookie = Cookie.SimpleCookie()
89
90 # Build headerMap
91 for item in headers.items():
92 # Warning: if there is more than one header entry for cookies (AFAIK, only Konqueror does that)
93 # only the last one will remain in headerMap (but they will be correctly stored in request.simpleCookie)
94 insertIntoHeaderMap(item[0],item[1])
95
96 # Handle cookies differently because on Konqueror, multiple cookies come on different lines with the same key
97 cookieList = headers.getallmatchingheaders('cookie')
98 for cookie in cookieList:
99 cpg.request.simpleCookie.load(cookie)
100
101 cpg.request.remoteAddr = clientAddress
102 cpg.request.remoteHost = remoteHost
103
104 # Set peer_certificate (in SSL mode) so the web app can examinate the client certificate
105 try: cpg.request.peerCertificate = self.request.get_peer_certificate()
106 except: pass
107
108 _cputil.getSpecialFunction('_cpLogMessage')("%s - %s" % (cpg.request.remoteAddr, requestLine[:-2]), "HTTP")
109
110
111 def parsePostData(rfile):
112 # Read request body and put it in data
113 len = int(cpg.request.headerMap.get("Content-Length","0"))
114 if len: data = rfile.read(len)
115 else: data=""
116
117 # Put data in a StringIO so FieldStorage can read it
118 newRfile = StringIO.StringIO(data)
119 # Create a copy of headerMap with lowercase keys because
120 # FieldStorage doesn't work otherwise
121 lowerHeaderMap = {}
122 for key, value in cpg.request.headerMap.items():
123 lowerHeaderMap[key.lower()] = value
124 forms = cgi.FieldStorage(fp = newRfile, headers = lowerHeaderMap, environ = {'REQUEST_METHOD':'POST'}, keep_blank_values = 1)
125 for key in forms.keys():
126 # Check if it's a list or not
127 valueList = forms[key]
128 if type(valueList) == type([]):
129 # It's a list of values
130 cpg.request.paramMap[key] = []
131 cpg.request.filenameMap[key] = []
132 cpg.request.fileTypeMap[key] = []
133 for item in valueList:
134 cpg.request.paramMap[key].append(item.value)
135 cpg.request.filenameMap[key].append(item.filename)
136 cpg.request.fileTypeMap[key].append(item.type)
137 else:
138 # It's a single value
139 # In case it's a file being uploaded, we save the filename in a map (user might need it)
140 cpg.request.paramMap[key] = valueList.value
141 cpg.request.filenameMap[key] = valueList.filename
142 cpg.request.fileTypeMap[key] = valueList.type
143
144 def applyFilterList(methodName):
145 try:
146 filterList = _cputil.getSpecialFunction('_cpFilterList')
147 for filter in filterList:
148 method = getattr(filter, methodName, None)
149 if method:
150 method()
151 except basefilter.InternalRedirect:
152 # If we get an InternalRedirect, we start the filter list
153 # from scratch. Is cpg.request.path or cpg.request.objectPath
154 # has been modified by the hook, then a new filter list
155 # will be applied.
156 # We use recursion so if there is an infinite loop, we'll
157 # get the regular python "recursion limit exceeded" exception.
158 applyFilterList(methodName)
159
160
161 def insertIntoHeaderMap(key,value):
162 normalizedKey = '-'.join([s.capitalize() for s in key.split('-')])
163 cpg.request.headerMap[normalizedKey] = value
164
165 def initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile):
166 parseFirstLine(requestLine)
167 cookHeaders(clientAddress, remoteHost, headers, requestLine)
168
169 cpg.request.base = "http://" + cpg.request.headerMap['Host']
170 cpg.request.browserUrl = cpg.request.base + cpg.request.browserUrl
171 cpg.request.isStatic = False
172 cpg.request.parsePostData = True
173 cpg.request.rfile = rfile
174
175 # Change objectPath in filters to change the object that will get rendered
176 cpg.request.objectPath = None
177
178 applyFilterList('afterRequestHeader')
179
180 if cpg.request.method == 'POST' and cpg.request.parsePostData:
181 parsePostData(rfile)
182
183 applyFilterList('afterRequestBody')
184
185 def doRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile):
186 # creates some attributes on cpg.response so filters can use them
187 cpg.response.wfile = wfile
188 cpg.response.sendResponse = 1
189 try:
190 initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile)
191 except basefilter.RequestHandled:
192 # request was already fully handled; it may be a cache hit
193 return
194
195 # Prepare response variables
196 now = time.time()
197 year, month, day, hh, mm, ss, wd, y, z = time.gmtime(now)
198 date = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (weekdayname[wd], day, monthname[month], year, hh, mm, ss)
199 cpg.response.headerMap = {
200 "protocolVersion": cpg.configOption.protocolVersion,
201 "Status": "200 OK",
202 "Content-Type": "text/html",
203 "Server": "CherryPy/" + cpg.__version__,
204 "Date": date,
205 "Set-Cookie": [],
206 "Content-Length": 0
207 }
208 cpg.response.simpleCookie = Cookie.SimpleCookie()
209
210 try:
211 handleRequest(cpg.response.wfile)
212 except:
213 # TODO: in some cases exceptions and filters are conflicting;
214 # error reporting seems to be broken in some cases. This code is
215 # a helper to check it
216 err = ""
217 exc_info_1 = sys.exc_info()[1]
218 if hasattr(exc_info_1, 'args') and len(exc_info_1.args) >= 1:
219 err = exc_info_1.args[0]
220
221 try:
222 _cputil.getSpecialFunction('_cpOnError')()
223
224 # Still save session data
225 if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
226 sessionId = cpg.response.simpleCookie[cpg.configOption.sessionCookieName].value
227 expirationTime = time.time() + cpg.configOption.sessionTimeout * 60
228 _cputil.getSpecialFunction('_cpSaveSessionData')(sessionId, cpg.request.sessionMap, expirationTime)
229
230 wfile.write('%s %s\r\n' % (cpg.response.headerMap['protocolVersion'], cpg.response.headerMap['Status']))
231
232 if (cpg.response.headerMap.has_key('Content-Length') and
233 cpg.response.headerMap['Content-Length']==0):
234 buf = StringIO.StringIO()
235 [buf.write(x) for x in cpg.response.body]
236 buf.seek(0)
237 cpg.response.body = [buf.read()]
238 cpg.response.headerMap['Content-Length'] = len(cpg.response.body[0])
239
240 for key, valueList in cpg.response.headerMap.items():
241 if key not in ('Status', 'protocolVersion'):
242 if type(valueList) != type([]): valueList = [valueList]
243 for value in valueList:
244 wfile.write('%s: %s\r\n'%(key, value))
245 wfile.write('\r\n')
246 for line in cpg.response.body:
247 wfile.write(line)
248 except:
249 bodyFile = StringIO.StringIO()
250 traceback.print_exc(file = bodyFile)
251 body = bodyFile.getvalue()
252 wfile.write('%s 200 OK\r\n' % cpg.configOption.protocolVersion)
253 wfile.write('Content-Type: text/plain\r\n')
254 wfile.write('Content-Length: %s\r\n' % len(body))
255 wfile.write('\r\n')
256 wfile.write(body)
257
258 def sendResponse(wfile):
259 applyFilterList('beforeResponse')
260
261 # Set the content-length
262 if (cpg.response.headerMap.has_key('Content-Length') and
263 cpg.response.headerMap['Content-Length']==0):
264 buf = StringIO.StringIO()
265 [buf.write(x) for x in cpg.response.body]
266 buf.seek(0)
267 cpg.response.body = [buf.read()]
268 cpg.response.headerMap['Content-Length'] = len(cpg.response.body[0])
269
270 # Save session data
271 if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
272 sessionId = cpg.response.simpleCookie[cpg.configOption.sessionCookieName].value
273 expirationTime = time.time() + cpg.configOption.sessionTimeout * 60
274 _cputil.getSpecialFunction('_cpSaveSessionData')(sessionId, cpg.request.sessionMap, expirationTime)
275
276 wfile.write('%s %s\r\n' % (cpg.response.headerMap['protocolVersion'], cpg.response.headerMap['Status']))
277 for key, valueList in cpg.response.headerMap.items():
278 if key not in ('Status', 'protocolVersion'):
279 if type(valueList) != type([]): valueList = [valueList]
280 for value in valueList:
281 wfile.write('%s: %s\r\n' % (key, value))
282
283 # Send response cookies
284 cookie = cpg.response.simpleCookie.output()
285 if cookie:
286 wfile.write(cookie+'\r\n')
287 wfile.write('\r\n')
288
289 for line in cpg.response.body:
290 wfile.write(line)
291
292 # finalization hook for filter cleanup & logging purposes
293 applyFilterList('afterResponse')
294
295 def handleRequest(wfile):
296 # Clean up expired sessions if needed:
297 now = time.time()
298 if cpg.configOption.sessionStorageType and cpg.configOption.sessionCleanUpDelay and cpg._lastSessionCleanUpTime + cpg.configOption.sessionCleanUpDelay * 60 <= now:
299 cpg._lastSessionCleanUpTime = now
300 _cputil.getSpecialFunction('_cpCleanUpOldSessions')()
301
302 # Save original values (in case they get modified by filters)
303 cpg.request.originalPath = cpg.request.path
304 cpg.request.originalParamMap = cpg.request.paramMap
305 cpg.request.originalParamList = cpg.request.paramList
306
307 path = cpg.request.path
308 if path.startswith('/'):
309 # Remove leading slash
310 path = path[1:]
311 if path.endswith('/'):
312 # Remove trailing slash
313 path = path[:-1]
314 path = urllib.unquote(path) # Replace quoted chars (eg %20) from url
315
316 # Handle static directories
317 for urlDir, fsDir in cpg.configOption.staticContentList:
318 if path == urlDir or path[:len(urlDir)+1]==urlDir+'/':
319
320 cpg.request.isStatic = 1
321
322 fname = fsDir + path[len(urlDir):]
323 start_url_var = cpg.request.browserUrl.find('?')
324 if start_url_var != -1: fname = fname + cpg.request.browserUrl[start_url_var:]
325 try:
326 stat = os.stat(fname)
327 except OSError:
328 raise cperror.NotFound
329 modifTime = stat.st_mtime
330
331 strModifTime = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(modifTime))
332
333 # Check if browser sent "if-modified-since" in request header
334 if cpg.request.headerMap.has_key('If-Modified-Since'):
335 # Check if if-modified-since date is the same as strModifTime
336 if cpg.request.headerMap['If-Modified-Since'] == strModifTime:
337 cpg.response.headerMap = {
338 'Status': 304,
339 'protocolVersion': cpg.configOption.protocolVersion,
340 'Date': cpg.response.headerMap['Date']}
341 cpg.response.body = []
342 sendResponse(wfile)
343 return
344
345 cpg.response.headerMap['Last-Modified'] = strModifTime
346 # Set Content-Length and use an iterable (file object)
347 # this way CP won't load the whole file in memory
348 cpg.response.headerMap['Content-Length'] = stat[6]
349 cpg.response.body = open(fname, 'rb')
350 # Set content-type based on filename extension
351 i = path.rfind('.')
352 if i != -1: ext = path[i:]
353 else: ext = ""
354 contentType = mimetypes.types_map.get(ext, "text/plain")
355 cpg.response.headerMap['Content-Type'] = contentType
356 sendResponse(wfile)
357 return
358
359 # Get session data
360 if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
361 now = time.time()
362 # First, get sessionId from cookie
363 try: sessionId = cpg.request.simpleCookie[cpg.configOption.sessionCookieName].value
364 except: sessionId=None
365 if sessionId:
366 # Load session data from wherever it was stored
367 sessionData = _cputil.getSpecialFunction('_cpLoadSessionData')(sessionId)
368 if sessionData == None:
369 sessionId = None
370 else:
371 cpg.request.sessionMap, expirationTime = sessionData
372 # Check that is hasn't expired
373 if now > expirationTime:
374 # Session expired
375 sessionId = None
376
377 # Create a new sessionId if needed
378 if not sessionId:
379 cpg.request.sessionMap = {}
380 sessionId = generateSessionId()
381 cpg.request.sessionMap['_sessionId'] = sessionId
382
383 cpg.response.simpleCookie[cpg.configOption.sessionCookieName] = sessionId
384 cpg.response.simpleCookie[cpg.configOption.sessionCookieName]['path'] = '/'
385 cpg.response.simpleCookie[cpg.configOption.sessionCookieName]['version'] = 1
386
387 try:
388 func, objectPathList, virtualPathList = mapPathToObject()
389 except IndexRedirect, inst:
390 # For an IndexRedirect, we don't go through the regular
391 # mechanism: we return the redirect immediately
392 newUrl = urlparse.urljoin(cpg.request.base, inst.args[0])
393 wfile.write('%s 302\r\n' % (cpg.response.headerMap['protocolVersion']))
394 cpg.response.headerMap['Location'] = newUrl
395 for key, valueList in cpg.response.headerMap.items():
396 if key not in ('Status', 'protocolVersion'):
397 if type(valueList) != type([]): valueList = [valueList]
398 for value in valueList:
399 wfile.write('%s: %s\r\n'%(key, value))
400 wfile.write('\r\n')
401 return
402
403 # Remove "root" from objectPathList and join it to get objectPath
404 cpg.request.objectPath = '/' + '/'.join(objectPathList[1:])
405 body = func(*(virtualPathList + cpg.request.paramList), **(cpg.request.paramMap))
406
407 # builds a uniform return type
408 if not isinstance(body, types.GeneratorType):
409 cpg.response.body = [body]
410 else:
411 cpg.response.body = body
412
413 if cpg.response.sendResponse:
414 sendResponse(wfile)
415
416 def generateSessionId():
417 s = ''
418 for i in range(50):
419 s += random.choice(string.letters+string.digits)
420 s += '%s'%time.time()
421 return hashlib.hashlib(s).hexdigest()
422
423 def getObjFromPath(objPathList, objCache):
424 """ For a given objectPathList (like ['root', 'a', 'b', 'index']),
425 return the object (or None if it doesn't exist).
426 Also keep a cache for maximum efficiency
427 """
428 # Let cpg be the first valid object.
429 validObjects = ["cpg"]
430
431 # Scan the objPathList in order from left to right
432 for index, obj in enumerate(objPathList):
433 # maps virtual filenames to Python identifiers (substitutes '.' for '_')
434 obj = obj.replace('.', '_')
435
436 # currentObjStr holds something like 'cpg.root.something.else'
437 currentObjStr = ".".join(validObjects)
438
439 #---------------
440 # Cache check
441 #---------------
442 # Generate a cacheKey from the first 'index' elements of objPathList
443 cacheKey = tuple(objPathList[:index+1])
444 # Is this cacheKey in the objCache?
445 if cacheKey in objCache:
446 # And is its value not None?
447 if objCache[cacheKey]:
448 # Yes, then add it to the list of validObjects
449 validObjects.append(obj)
450 # OK, go to the next iteration
451 continue
452 # Its value is None, so we stop
453 # (This means it is not a valid object)
454 break
455
456 #-----------------
457 # Attribute check
458 #-----------------
459 if getattr(eval(currentObjStr), obj, None):
460 # obj is a valid attribute of the current object
461 validObjects.append(obj)
462 # Store it in the cache
463 objCache[cacheKey] = eval(".".join(validObjects))
464 else:
465 # obj is not a valid attribute
466 # Store None in the cache
467 objCache[cacheKey] = None
468 # Stop, we won't process the remaining objPathList
469 break
470
471 # Return the last cached object (even if its None)
472 return objCache[cacheKey]
473
474 def mapPathToObject(path = None):
475 # Traverse path:
476 # for /a/b?arg=val, we'll try:
477 # root.a.b.index -> redirect to /a/b/?arg=val
478 # root.a.b.default(arg='val') -> redirect to /a/b/?arg=val
479 # root.a.b(arg='val')
480 # root.a.default('b', arg='val')
481 # root.default('a', 'b', arg='val')
482
483 # Also, we ignore trailing slashes
484 # Also, a method has to have ".exposed = True" in order to be exposed
485
486 if path is None:
487 path = cpg.request.objectPath or cpg.request.path
488 if path.startswith('/'):
489 path = path[1:] # Remove leading slash
490 if path.endswith('/'):
491 path = path[:-1] # Remove trailing slash
492
493 if not path:
494 objectPathList = []
495 else:
496 objectPathList = path.split('/')
497 objectPathList = ['root'] + objectPathList + ['index']
498
499 # Try successive objects... (and also keep the remaining object list)
500 objCache = {}
501 isFirst = True
502 isSecond = False
503 isDefault = False
504 foundIt = False
505 virtualPathList = []
506 while objectPathList:
507 if isFirst or isSecond:
508 # Only try this for a.b.index() or a.b()
509 candidate = getObjFromPath(objectPathList, objCache)
510 if callable(candidate) and getattr(candidate, 'exposed', False):
511 foundIt = True
512 break
513 # Couldn't find the object: pop one from the list and try "default"
514 lastObj = objectPathList.pop()
515 if (not isFirst) or (not path):
516 virtualPathList.insert(0, lastObj)
517 objectPathList.append('default')
518 candidate = getObjFromPath(objectPathList, objCache)
519 if callable(candidate) and getattr(candidate, 'exposed', False):
520 foundIt = True
521 isDefault = True
522 break
523 objectPathList.pop() # Remove "default"
524 if isSecond:
525 isSecond = False
526 if isFirst:
527 isFirst = False
528 isSecond = True
529
530 # Check results of traversal
531 if not foundIt:
532 raise cperror.NotFound # We didn't find anything
533
534 if isFirst:
535 # We found the extra ".index"
536 # Check if the original path had a trailing slash (otherwise, do
537 # a redirect)
538 if cpg.request.path[-1] != '/':
539 newUrl = cpg.request.path + '/'
540 if cpg.request.queryString: newUrl += cpg.request.queryString
541 raise IndexRedirect(newUrl)
542
543 return candidate, objectPathList, virtualPathList