Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/tags/cherrypy-2.0.0/cherrypy/_cphttptools.py

Revision 159 (checked in by rdelon, 3 years ago)

Fixed ticket #78; Try root.default for / if root.index doesn't exist

Line 
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, sha, 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