source: python/vendor/Python-2.7.6/Lib/CGIHTTPServer.py

Last change on this file was 388, checked in by dmik, 11 years ago

python: Update vendor to 2.7.6.

  • Property svn:eol-style set to native
File size: 12.8 KB
Line 
1"""CGI-savvy HTTP Server.
2
3This module builds on SimpleHTTPServer by implementing GET and POST
4requests to cgi-bin scripts.
5
6If the os.fork() function is not present (e.g. on Windows),
7os.popen2() is used as a fallback, with slightly altered semantics; if
8that function is not present either (e.g. on Macintosh), only Python
9scripts are supported, and they are executed by the current process.
10
11In all cases, the implementation is intentionally naive -- all
12requests are executed sychronously.
13
14SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
15-- it may execute arbitrary Python code or external programs.
16
17Note that status code 200 is sent prior to execution of a CGI script, so
18scripts cannot send other status codes such as 302 (redirect).
19"""
20
21
22__version__ = "0.4"
23
24__all__ = ["CGIHTTPRequestHandler"]
25
26import os
27import sys
28import urllib
29import BaseHTTPServer
30import SimpleHTTPServer
31import select
32import copy
33
34
35class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
36
37 """Complete HTTP server with GET, HEAD and POST commands.
38
39 GET and HEAD also support running CGI scripts.
40
41 The POST command is *only* implemented for CGI scripts.
42
43 """
44
45 # Determine platform specifics
46 have_fork = hasattr(os, 'fork')
47 have_popen2 = hasattr(os, 'popen2')
48 have_popen3 = hasattr(os, 'popen3')
49
50 # Make rfile unbuffered -- we need to read one line and then pass
51 # the rest to a subprocess, so we can't use buffered input.
52 rbufsize = 0
53
54 def do_POST(self):
55 """Serve a POST request.
56
57 This is only implemented for CGI scripts.
58
59 """
60
61 if self.is_cgi():
62 self.run_cgi()
63 else:
64 self.send_error(501, "Can only POST to CGI scripts")
65
66 def send_head(self):
67 """Version of send_head that support CGI scripts"""
68 if self.is_cgi():
69 return self.run_cgi()
70 else:
71 return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
72
73 def is_cgi(self):
74 """Test whether self.path corresponds to a CGI script.
75
76 Returns True and updates the cgi_info attribute to the tuple
77 (dir, rest) if self.path requires running a CGI script.
78 Returns False otherwise.
79
80 If any exception is raised, the caller should assume that
81 self.path was rejected as invalid and act accordingly.
82
83 The default implementation tests whether the normalized url
84 path begins with one of the strings in self.cgi_directories
85 (and the next character is a '/' or the end of the string).
86 """
87 collapsed_path = _url_collapse_path(self.path)
88 dir_sep = collapsed_path.find('/', 1)
89 head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:]
90 if head in self.cgi_directories:
91 self.cgi_info = head, tail
92 return True
93 return False
94
95 cgi_directories = ['/cgi-bin', '/htbin']
96
97 def is_executable(self, path):
98 """Test whether argument path is an executable file."""
99 return executable(path)
100
101 def is_python(self, path):
102 """Test whether argument path is a Python script."""
103 head, tail = os.path.splitext(path)
104 return tail.lower() in (".py", ".pyw")
105
106 def run_cgi(self):
107 """Execute a CGI script."""
108 dir, rest = self.cgi_info
109
110 i = rest.find('/')
111 while i >= 0:
112 nextdir = rest[:i]
113 nextrest = rest[i+1:]
114
115 scriptdir = self.translate_path(nextdir)
116 if os.path.isdir(scriptdir):
117 dir, rest = nextdir, nextrest
118 i = rest.find('/')
119 else:
120 break
121
122 # find an explicit query string, if present.
123 i = rest.rfind('?')
124 if i >= 0:
125 rest, query = rest[:i], rest[i+1:]
126 else:
127 query = ''
128
129 # dissect the part after the directory name into a script name &
130 # a possible additional path, to be stored in PATH_INFO.
131 i = rest.find('/')
132 if i >= 0:
133 script, rest = rest[:i], rest[i:]
134 else:
135 script, rest = rest, ''
136
137 scriptname = dir + '/' + script
138 scriptfile = self.translate_path(scriptname)
139 if not os.path.exists(scriptfile):
140 self.send_error(404, "No such CGI script (%r)" % scriptname)
141 return
142 if not os.path.isfile(scriptfile):
143 self.send_error(403, "CGI script is not a plain file (%r)" %
144 scriptname)
145 return
146 ispy = self.is_python(scriptname)
147 if not ispy:
148 if not (self.have_fork or self.have_popen2 or self.have_popen3):
149 self.send_error(403, "CGI script is not a Python script (%r)" %
150 scriptname)
151 return
152 if not self.is_executable(scriptfile):
153 self.send_error(403, "CGI script is not executable (%r)" %
154 scriptname)
155 return
156
157 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
158 # XXX Much of the following could be prepared ahead of time!
159 env = copy.deepcopy(os.environ)
160 env['SERVER_SOFTWARE'] = self.version_string()
161 env['SERVER_NAME'] = self.server.server_name
162 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
163 env['SERVER_PROTOCOL'] = self.protocol_version
164 env['SERVER_PORT'] = str(self.server.server_port)
165 env['REQUEST_METHOD'] = self.command
166 uqrest = urllib.unquote(rest)
167 env['PATH_INFO'] = uqrest
168 env['PATH_TRANSLATED'] = self.translate_path(uqrest)
169 env['SCRIPT_NAME'] = scriptname
170 if query:
171 env['QUERY_STRING'] = query
172 host = self.address_string()
173 if host != self.client_address[0]:
174 env['REMOTE_HOST'] = host
175 env['REMOTE_ADDR'] = self.client_address[0]
176 authorization = self.headers.getheader("authorization")
177 if authorization:
178 authorization = authorization.split()
179 if len(authorization) == 2:
180 import base64, binascii
181 env['AUTH_TYPE'] = authorization[0]
182 if authorization[0].lower() == "basic":
183 try:
184 authorization = base64.decodestring(authorization[1])
185 except binascii.Error:
186 pass
187 else:
188 authorization = authorization.split(':')
189 if len(authorization) == 2:
190 env['REMOTE_USER'] = authorization[0]
191 # XXX REMOTE_IDENT
192 if self.headers.typeheader is None:
193 env['CONTENT_TYPE'] = self.headers.type
194 else:
195 env['CONTENT_TYPE'] = self.headers.typeheader
196 length = self.headers.getheader('content-length')
197 if length:
198 env['CONTENT_LENGTH'] = length
199 referer = self.headers.getheader('referer')
200 if referer:
201 env['HTTP_REFERER'] = referer
202 accept = []
203 for line in self.headers.getallmatchingheaders('accept'):
204 if line[:1] in "\t\n\r ":
205 accept.append(line.strip())
206 else:
207 accept = accept + line[7:].split(',')
208 env['HTTP_ACCEPT'] = ','.join(accept)
209 ua = self.headers.getheader('user-agent')
210 if ua:
211 env['HTTP_USER_AGENT'] = ua
212 co = filter(None, self.headers.getheaders('cookie'))
213 if co:
214 env['HTTP_COOKIE'] = ', '.join(co)
215 # XXX Other HTTP_* headers
216 # Since we're setting the env in the parent, provide empty
217 # values to override previously set values
218 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
219 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
220 env.setdefault(k, "")
221
222 self.send_response(200, "Script output follows")
223
224 decoded_query = query.replace('+', ' ')
225
226 if self.have_fork:
227 # Unix -- fork as we should
228 args = [script]
229 if '=' not in decoded_query:
230 args.append(decoded_query)
231 nobody = nobody_uid()
232 self.wfile.flush() # Always flush before forking
233 pid = os.fork()
234 if pid != 0:
235 # Parent
236 pid, sts = os.waitpid(pid, 0)
237 # throw away additional data [see bug #427345]
238 while select.select([self.rfile], [], [], 0)[0]:
239 if not self.rfile.read(1):
240 break
241 if sts:
242 self.log_error("CGI script exit status %#x", sts)
243 return
244 # Child
245 try:
246 try:
247 os.setuid(nobody)
248 except os.error:
249 pass
250 os.dup2(self.rfile.fileno(), 0)
251 os.dup2(self.wfile.fileno(), 1)
252 os.execve(scriptfile, args, env)
253 except:
254 self.server.handle_error(self.request, self.client_address)
255 os._exit(127)
256
257 else:
258 # Non Unix - use subprocess
259 import subprocess
260 cmdline = [scriptfile]
261 if self.is_python(scriptfile):
262 interp = sys.executable
263 if interp.lower().endswith("w.exe"):
264 # On Windows, use python.exe, not pythonw.exe
265 interp = interp[:-5] + interp[-4:]
266 cmdline = [interp, '-u'] + cmdline
267 if '=' not in query:
268 cmdline.append(query)
269
270 self.log_message("command: %s", subprocess.list2cmdline(cmdline))
271 try:
272 nbytes = int(length)
273 except (TypeError, ValueError):
274 nbytes = 0
275 p = subprocess.Popen(cmdline,
276 stdin = subprocess.PIPE,
277 stdout = subprocess.PIPE,
278 stderr = subprocess.PIPE,
279 env = env
280 )
281 if self.command.lower() == "post" and nbytes > 0:
282 data = self.rfile.read(nbytes)
283 else:
284 data = None
285 # throw away additional data [see bug #427345]
286 while select.select([self.rfile._sock], [], [], 0)[0]:
287 if not self.rfile._sock.recv(1):
288 break
289 stdout, stderr = p.communicate(data)
290 self.wfile.write(stdout)
291 if stderr:
292 self.log_error('%s', stderr)
293 p.stderr.close()
294 p.stdout.close()
295 status = p.returncode
296 if status:
297 self.log_error("CGI script exit status %#x", status)
298 else:
299 self.log_message("CGI script exited OK")
300
301
302def _url_collapse_path(path):
303 """
304 Given a URL path, remove extra '/'s and '.' path elements and collapse
305 any '..' references and returns a colllapsed path.
306
307 Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
308 The utility of this function is limited to is_cgi method and helps
309 preventing some security attacks.
310
311 Returns: A tuple of (head, tail) where tail is everything after the final /
312 and head is everything before it. Head will always start with a '/' and,
313 if it contains anything else, never have a trailing '/'.
314
315 Raises: IndexError if too many '..' occur within the path.
316
317 """
318 # Similar to os.path.split(os.path.normpath(path)) but specific to URL
319 # path semantics rather than local operating system semantics.
320 path_parts = path.split('/')
321 head_parts = []
322 for part in path_parts[:-1]:
323 if part == '..':
324 head_parts.pop() # IndexError if more '..' than prior parts
325 elif part and part != '.':
326 head_parts.append( part )
327 if path_parts:
328 tail_part = path_parts.pop()
329 if tail_part:
330 if tail_part == '..':
331 head_parts.pop()
332 tail_part = ''
333 elif tail_part == '.':
334 tail_part = ''
335 else:
336 tail_part = ''
337
338 splitpath = ('/' + '/'.join(head_parts), tail_part)
339 collapsed_path = "/".join(splitpath)
340
341 return collapsed_path
342
343
344nobody = None
345
346def nobody_uid():
347 """Internal routine to get nobody's uid"""
348 global nobody
349 if nobody:
350 return nobody
351 try:
352 import pwd
353 except ImportError:
354 return -1
355 try:
356 nobody = pwd.getpwnam('nobody')[2]
357 except KeyError:
358 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
359 return nobody
360
361
362def executable(path):
363 """Test for executable file."""
364 try:
365 st = os.stat(path)
366 except os.error:
367 return False
368 return st.st_mode & 0111 != 0
369
370
371def test(HandlerClass = CGIHTTPRequestHandler,
372 ServerClass = BaseHTTPServer.HTTPServer):
373 SimpleHTTPServer.test(HandlerClass, ServerClass)
374
375
376if __name__ == '__main__':
377 test()
Note: See TracBrowser for help on using the repository browser.