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

Ticket #104: _cphttptools.2.py

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
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     initRequest(clientAddress, remoteHost, requestLine, headers, rfile, wfile)
187
188     # Prepare response variables
189     now = time.time()
190     year, month, day, hh, mm, ss, wd, y, z = time.gmtime(now)
191     date = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (weekdayname[wd], day, monthname[month], year, hh, mm, ss)
192     cpg.response.headerMap = {
193         "protocolVersion": cpg.configOption.protocolVersion,
194         "Status": "200 OK",
195         "Content-Type": "text/html",
196         "Server": "CherryPy/" + cpg.__version__,
197         "Date": date,
198         "Set-Cookie": [],
199         "Content-Length": 0
200     }
201     cpg.response.simpleCookie = Cookie.SimpleCookie()
202     cpg.response.wfile = wfile
203     cpg.response.sendResponse = 1
204
205     try:
206         handleRequest(wfile)
207     except:
208         err = ""
209         exc_info_1 = sys.exc_info()[1]
210         if hasattr(exc_info_1, 'args') and len(exc_info_1.args) >= 1:
211             err = exc_info_1.args[0]
212
213         try:
214             _cputil.getSpecialFunction('_cpOnError')()
215
216             # Still save session data
217             if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
218                 sessionId = cpg.response.simpleCookie[cpg.configOption.sessionCookieName].value
219                 expirationTime = time.time() + cpg.configOption.sessionTimeout * 60
220                 _cputil.getSpecialFunction('_cpSaveSessionData')(sessionId, cpg.request.sessionMap, expirationTime)
221
222             wfile.write('%s %s\r\n' % (cpg.response.headerMap['protocolVersion'], cpg.response.headerMap['Status']))
223
224             if (cpg.response.headerMap.has_key('Content-Length') and
225                     cpg.response.headerMap['Content-Length']==0):
226                         buf = StringIO.StringIO()
227                         [buf.write(x) for x in cpg.response.body]
228                         buf.seek(0)
229                         cpg.response.body = [buf.read()]
230                         cpg.response.headerMap['Content-Length'] = len(cpg.response.body[0])
231
232             for key, valueList in cpg.response.headerMap.items():
233                 if key not in ('Status', 'protocolVersion'):
234                     if type(valueList) != type([]): valueList = [valueList]
235                     for value in valueList:
236                         wfile.write('%s: %s\r\n'%(key, value))
237             wfile.write('\r\n')
238             for line in cpg.response.body:
239                 wfile.write(line)
240         except:
241             bodyFile = StringIO.StringIO()
242             traceback.print_exc(file = bodyFile)
243             body = bodyFile.getvalue()
244             wfile.write('%s 200 OK\r\n' % cpg.configOption.protocolVersion)
245             wfile.write('Content-Type: text/plain\r\n')
246             wfile.write('Content-Length: %s\r\n' % len(body))
247             wfile.write('\r\n')
248             wfile.write(body)
249
250 def sendResponse(wfile):
251     applyFilterList('beforeResponse')
252
253     # Save session data
254     if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
255         sessionId = cpg.response.simpleCookie[cpg.configOption.sessionCookieName].value
256         expirationTime = time.time() + cpg.configOption.sessionTimeout * 60
257         _cputil.getSpecialFunction('_cpSaveSessionData')(sessionId, cpg.request.sessionMap, expirationTime)
258
259     # Set the content-length
260     if (cpg.response.headerMap.has_key('Content-Length') and
261             cpg.response.headerMap['Content-Length']==0):
262         buf = StringIO.StringIO()
263         [buf.write(x) for x in cpg.response.body]
264         buf.seek(0)
265         cpg.response.body = [buf.read()]
266         cpg.response.headerMap['Content-Length'] = len(cpg.response.body[0])
267
268     wfile.write('%s %s\r\n' % (cpg.response.headerMap['protocolVersion'], cpg.response.headerMap['Status']))
269     for key, valueList in cpg.response.headerMap.items():
270         if key not in ('Status', 'protocolVersion'):
271             if type(valueList) != type([]): valueList = [valueList]
272             for value in valueList:
273                 wfile.write('%s: %s\r\n'%(key, value))
274
275     # Send response cookies
276     cookie = cpg.response.simpleCookie.output()
277     if cookie:
278         wfile.write(cookie+'\r\n')
279     wfile.write('\r\n')
280
281     for line in cpg.response.body:
282         wfile.write(line)
283
284     # finalization hook for filter cleanup & logging purposes
285     applyFilterList('afterResponse')
286
287 def handleRequest(wfile):
288     # Clean up expired sessions if needed:
289     now = time.time()
290     if cpg.configOption.sessionStorageType and cpg.configOption.sessionCleanUpDelay and cpg._lastSessionCleanUpTime + cpg.configOption.sessionCleanUpDelay * 60 <= now:
291         cpg._lastSessionCleanUpTime = now
292         _cputil.getSpecialFunction('_cpCleanUpOldSessions')()
293
294     # Save original values (in case they get modified by filters)
295     cpg.request.originalPath = cpg.request.path
296     cpg.request.originalParamMap = cpg.request.paramMap
297     cpg.request.originalParamList = cpg.request.paramList
298
299     path = cpg.request.path
300     if path.startswith('/'): path = path[1:] # Remove leading slash
301     if path.endswith('/'): path = path[:-1] # Remove trailing slash
302     path = urllib.unquote(path) # Replace quoted chars (eg %20) from url
303
304     # Handle static directories
305     for urlDir, fsDir in cpg.configOption.staticContentList:
306         if path == urlDir or path[:len(urlDir)+1]==urlDir+'/':
307             print 'static'
308
309             cpg.request.isStatic = 1
310
311             fname = fsDir + path[len(urlDir):]
312             try:
313                 stat = os.stat(fname)
314             except OSError:
315                 raise cperror.NotFound
316             if type(stat) == type(()): # Python2.1
317                 modifTime = stat[9]
318             else:
319                 modifTime = stat.st_mtime
320
321             strModifTime = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(modifTime))
322
323             # Check if browser sent "if-modified-since" in request header
324             if cpg.request.headerMap.has_key('If-Modified-Since'):
325                 # Check if if-modified-since date is the same as strModifTime
326                 if cpg.request.headerMap['If-Modified-Since'] == strModifTime:
327                     cpg.response.headerMap = {'Status': 304, 'protocolVersion': cpg.configOption.protocolVersion, 'Date': cpg.response.headerMap['Date']}
328                     cpg.response.body = ''
329                     sendResponse(wfile)
330                     return
331
332             cpg.response.headerMap['Last-Modified'] = strModifTime
333             f=open(fname, 'rb')
334             cpg.response.body = f.read()
335             f.close()
336             # Set content-type based on filename extension
337             i = path.rfind('.')
338             if i != -1: ext = path[i:]
339             else: ext = ""
340             contentType = mimetypes.types_map.get(ext, "text/plain")
341             cpg.response.headerMap['Content-Type'] = contentType
342             sendResponse(wfile)
343             return
344
345     # Get session data
346     if cpg.configOption.sessionStorageType and not cpg.request.isStatic:
347         now = time.time()
348         # First, get sessionId from cookie
349         try: sessionId = cpg.request.simpleCookie[cpg.configOption.sessionCookieName].value
350         except: sessionId=None
351         if sessionId:
352             # Load session data from wherever it was stored
353             sessionData = _cputil.getSpecialFunction('_cpLoadSessionData')(sessionId)
354             if sessionData == None:
355                 sessionId = None
356             else:
357                 cpg.request.sessionMap, expirationTime = sessionData
358                 # Check that is hasn't expired
359                 if now > expirationTime:
360                     # Session expired
361                     sessionId = None
362
363         # Create a new sessionId if needed
364         if not sessionId:
365             cpg.request.sessionMap = {}
366             sessionId = generateSessionId()
367             cpg.request.sessionMap['_sessionId'] = sessionId
368
369         cpg.response.simpleCookie[cpg.configOption.sessionCookieName] = sessionId
370         cpg.response.simpleCookie[cpg.configOption.sessionCookieName]['path'] = '/'
371         cpg.response.simpleCookie[cpg.configOption.sessionCookieName]['version'] = 1
372
373     try:
374         func, objectPathList, virtualPathList = mapPathToObject()
375     except IndexRedirect, inst:
376         # For an IndexRedirect, we don't go through the regular
377         #   mechanism: we return the redirect immediately
378         newUrl = canonicalizeUrl(inst.args[0])
379         wfile.write('%s 302\r\n' % (cpg.response.headerMap['protocolVersion']))
380         cpg.response.headerMap['Location'] = newUrl
381         for key, valueList in cpg.response.headerMap.items():
382             if key not in ('Status', 'protocolVersion'):
383                 if type(valueList) != type([]): valueList = [valueList]
384                 for value in valueList:
385                     wfile.write('%s: %s\r\n'%(key