blob: 61f34f82bcc212508583bc9424b7a17eccd2e7df [file] [log] [blame]
Guido van Rossume7e578f1995-08-04 04:00:20 +00001"""CGI-savvy HTTP Server.
2
3This module builds on SimpleHTTPServer by implementing GET and POST
4requests to cgi-bin scripts.
5
Guido van Rossume7d6b0a2000-09-19 04:01:01 +00006If 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.
Fred Drake40e84db1999-10-16 02:07:50 +000016
Jeremy Hylton6414cd82004-12-22 14:19:09 +000017Note that status code 200 is sent prior to execution of a CGI script, so
18scripts cannot send other status codes such as 302 (redirect).
Guido van Rossume7e578f1995-08-04 04:00:20 +000019"""
20
21
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000022__version__ = "0.4"
Guido van Rossume7e578f1995-08-04 04:00:20 +000023
Skip Montanaroe99d5ea2001-01-20 19:54:20 +000024__all__ = ["CGIHTTPRequestHandler"]
Guido van Rossume7e578f1995-08-04 04:00:20 +000025
26import os
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000027import sys
Guido van Rossume7e578f1995-08-04 04:00:20 +000028import urllib
29import BaseHTTPServer
30import SimpleHTTPServer
Steve Holden8a978f72003-01-08 18:53:18 +000031import select
Senthil Kumarana9bd0cc2010-10-03 18:16:52 +000032import copy
Guido van Rossume7e578f1995-08-04 04:00:20 +000033
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
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000045 # Determine platform specifics
46 have_fork = hasattr(os, 'fork')
47 have_popen2 = hasattr(os, 'popen2')
Guido van Rossum8cb65402002-02-01 16:27:59 +000048 have_popen3 = hasattr(os, 'popen3')
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000049
Guido van Rossum6aefd912000-09-01 03:27:34 +000050 # 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
Guido van Rossume7e578f1995-08-04 04:00:20 +000054 def do_POST(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000055 """Serve a POST request.
Guido van Rossume7e578f1995-08-04 04:00:20 +000056
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000057 This is only implemented for CGI scripts.
Guido van Rossume7e578f1995-08-04 04:00:20 +000058
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000059 """
Guido van Rossume7e578f1995-08-04 04:00:20 +000060
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000061 if self.is_cgi():
62 self.run_cgi()
63 else:
64 self.send_error(501, "Can only POST to CGI scripts")
Guido van Rossume7e578f1995-08-04 04:00:20 +000065
66 def send_head(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000067 """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)
Guido van Rossume7e578f1995-08-04 04:00:20 +000072
73 def is_cgi(self):
Gregory P. Smith923ba362009-04-06 06:33:26 +000074 """Test whether self.path corresponds to a CGI script.
Guido van Rossume7e578f1995-08-04 04:00:20 +000075
Gregory P. Smith923ba362009-04-06 06:33:26 +000076 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.
Guido van Rossume7e578f1995-08-04 04:00:20 +000079
Gregory P. Smith4bd76642009-05-03 20:27:25 +000080 If any exception is raised, the caller should assume that
81 self.path was rejected as invalid and act accordingly.
82
Gregory P. Smith923ba362009-04-06 06:33:26 +000083 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).
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000086 """
Gregory P. Smith923ba362009-04-06 06:33:26 +000087 splitpath = _url_collapse_path_split(self.path)
Senthil Kumaranfb2e8742012-04-11 03:07:57 +080088 joined_path = '/'.join(splitpath)
89 dir_sep = joined_path.find('/', 1)
90 head, tail = joined_path[:dir_sep], joined_path[dir_sep+1:]
91 if head in self.cgi_directories:
92 self.cgi_info = head, tail
Gregory P. Smith923ba362009-04-06 06:33:26 +000093 return True
Tim Petersbc0e9102002-04-04 22:55:58 +000094 return False
Guido van Rossume7e578f1995-08-04 04:00:20 +000095
96 cgi_directories = ['/cgi-bin', '/htbin']
97
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000098 def is_executable(self, path):
99 """Test whether argument path is an executable file."""
100 return executable(path)
101
102 def is_python(self, path):
103 """Test whether argument path is a Python script."""
104 head, tail = os.path.splitext(path)
105 return tail.lower() in (".py", ".pyw")
106
Guido van Rossume7e578f1995-08-04 04:00:20 +0000107 def run_cgi(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000108 """Execute a CGI script."""
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000109 path = self.path
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000110 dir, rest = self.cgi_info
Tim Petersf733abb2007-01-30 03:03:46 +0000111
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000112 i = path.find('/', len(dir) + 1)
113 while i >= 0:
114 nextdir = path[:i]
115 nextrest = path[i+1:]
116
117 scriptdir = self.translate_path(nextdir)
118 if os.path.isdir(scriptdir):
119 dir, rest = nextdir, nextrest
120 i = path.find('/', len(dir) + 1)
121 else:
122 break
123
124 # find an explicit query string, if present.
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000125 i = rest.rfind('?')
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000126 if i >= 0:
127 rest, query = rest[:i], rest[i+1:]
128 else:
129 query = ''
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000130
131 # dissect the part after the directory name into a script name &
132 # a possible additional path, to be stored in PATH_INFO.
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000133 i = rest.find('/')
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000134 if i >= 0:
135 script, rest = rest[:i], rest[i:]
136 else:
137 script, rest = rest, ''
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000138
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000139 scriptname = dir + '/' + script
140 scriptfile = self.translate_path(scriptname)
141 if not os.path.exists(scriptfile):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000142 self.send_error(404, "No such CGI script (%r)" % scriptname)
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000143 return
144 if not os.path.isfile(scriptfile):
Tim Peters27f49612004-03-20 21:51:12 +0000145 self.send_error(403, "CGI script is not a plain file (%r)" %
Walter Dörwald70a6b492004-02-12 17:35:32 +0000146 scriptname)
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000147 return
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000148 ispy = self.is_python(scriptname)
149 if not ispy:
Guido van Rossum8cb65402002-02-01 16:27:59 +0000150 if not (self.have_fork or self.have_popen2 or self.have_popen3):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000151 self.send_error(403, "CGI script is not a Python script (%r)" %
152 scriptname)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000153 return
154 if not self.is_executable(scriptfile):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000155 self.send_error(403, "CGI script is not executable (%r)" %
156 scriptname)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000157 return
158
159 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
160 # XXX Much of the following could be prepared ahead of time!
Senthil Kumarana9bd0cc2010-10-03 18:16:52 +0000161 env = copy.deepcopy(os.environ)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000162 env['SERVER_SOFTWARE'] = self.version_string()
163 env['SERVER_NAME'] = self.server.server_name
164 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
165 env['SERVER_PROTOCOL'] = self.protocol_version
166 env['SERVER_PORT'] = str(self.server.server_port)
167 env['REQUEST_METHOD'] = self.command
168 uqrest = urllib.unquote(rest)
169 env['PATH_INFO'] = uqrest
170 env['PATH_TRANSLATED'] = self.translate_path(uqrest)
171 env['SCRIPT_NAME'] = scriptname
172 if query:
173 env['QUERY_STRING'] = query
174 host = self.address_string()
175 if host != self.client_address[0]:
176 env['REMOTE_HOST'] = host
177 env['REMOTE_ADDR'] = self.client_address[0]
Martin v. Löwisa28b3e62004-08-29 16:53:26 +0000178 authorization = self.headers.getheader("authorization")
179 if authorization:
180 authorization = authorization.split()
181 if len(authorization) == 2:
182 import base64, binascii
183 env['AUTH_TYPE'] = authorization[0]
184 if authorization[0].lower() == "basic":
185 try:
186 authorization = base64.decodestring(authorization[1])
187 except binascii.Error:
188 pass
189 else:
190 authorization = authorization.split(':')
191 if len(authorization) == 2:
192 env['REMOTE_USER'] = authorization[0]
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000193 # XXX REMOTE_IDENT
194 if self.headers.typeheader is None:
195 env['CONTENT_TYPE'] = self.headers.type
196 else:
197 env['CONTENT_TYPE'] = self.headers.typeheader
198 length = self.headers.getheader('content-length')
199 if length:
200 env['CONTENT_LENGTH'] = length
Collin Winter83b2bf62007-03-09 03:15:56 +0000201 referer = self.headers.getheader('referer')
202 if referer:
203 env['HTTP_REFERER'] = referer
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000204 accept = []
205 for line in self.headers.getallmatchingheaders('accept'):
Eric S. Raymond7e642e82001-02-09 12:10:26 +0000206 if line[:1] in "\t\n\r ":
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000207 accept.append(line.strip())
Guido van Rossum01fc65d1998-05-13 20:13:24 +0000208 else:
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000209 accept = accept + line[7:].split(',')
210 env['HTTP_ACCEPT'] = ','.join(accept)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000211 ua = self.headers.getheader('user-agent')
212 if ua:
213 env['HTTP_USER_AGENT'] = ua
214 co = filter(None, self.headers.getheaders('cookie'))
215 if co:
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000216 env['HTTP_COOKIE'] = ', '.join(co)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000217 # XXX Other HTTP_* headers
Guido van Rossum70ec0b42004-03-20 22:18:03 +0000218 # Since we're setting the env in the parent, provide empty
219 # values to override previously set values
220 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
Collin Winter83b2bf62007-03-09 03:15:56 +0000221 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
Guido van Rossum70ec0b42004-03-20 22:18:03 +0000222 env.setdefault(k, "")
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000223
224 self.send_response(200, "Script output follows")
225
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000226 decoded_query = query.replace('+', ' ')
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000227
228 if self.have_fork:
229 # Unix -- fork as we should
230 args = [script]
231 if '=' not in decoded_query:
232 args.append(decoded_query)
233 nobody = nobody_uid()
234 self.wfile.flush() # Always flush before forking
235 pid = os.fork()
236 if pid != 0:
237 # Parent
238 pid, sts = os.waitpid(pid, 0)
Steve Holden8a978f72003-01-08 18:53:18 +0000239 # throw away additional data [see bug #427345]
240 while select.select([self.rfile], [], [], 0)[0]:
Raymond Hettingere2f18372003-06-29 05:06:56 +0000241 if not self.rfile.read(1):
242 break
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000243 if sts:
244 self.log_error("CGI script exit status %#x", sts)
245 return
246 # Child
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000247 try:
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000248 try:
249 os.setuid(nobody)
250 except os.error:
251 pass
252 os.dup2(self.rfile.fileno(), 0)
253 os.dup2(self.wfile.fileno(), 1)
Senthil Kumarana9bd0cc2010-10-03 18:16:52 +0000254 os.execve(scriptfile, args, env)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000255 except:
256 self.server.handle_error(self.request, self.client_address)
257 os._exit(127)
258
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000259 else:
260 # Non Unix - use subprocess
261 import subprocess
262 cmdline = [scriptfile]
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000263 if self.is_python(scriptfile):
264 interp = sys.executable
265 if interp.lower().endswith("w.exe"):
Guido van Rossum0afde132001-10-26 03:38:46 +0000266 # On Windows, use python.exe, not pythonw.exe
267 interp = interp[:-5] + interp[-4:]
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000268 cmdline = [interp, '-u'] + cmdline
269 if '=' not in query:
270 cmdline.append(query)
271
272 self.log_message("command: %s", subprocess.list2cmdline(cmdline))
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000273 try:
274 nbytes = int(length)
Guido van Rossumb3903152002-10-17 16:21:35 +0000275 except (TypeError, ValueError):
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000276 nbytes = 0
Senthil Kumaran5dff3542009-11-11 17:22:35 +0000277 p = subprocess.Popen(cmdline,
278 stdin = subprocess.PIPE,
279 stdout = subprocess.PIPE,
Senthil Kumarana9bd0cc2010-10-03 18:16:52 +0000280 stderr = subprocess.PIPE,
281 env = env
Senthil Kumaran5dff3542009-11-11 17:22:35 +0000282 )
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000283 if self.command.lower() == "post" and nbytes > 0:
284 data = self.rfile.read(nbytes)
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000285 else:
286 data = None
Steve Holden8a978f72003-01-08 18:53:18 +0000287 # throw away additional data [see bug #427345]
288 while select.select([self.rfile._sock], [], [], 0)[0]:
Raymond Hettingere2f18372003-06-29 05:06:56 +0000289 if not self.rfile._sock.recv(1):
290 break
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000291 stdout, stderr = p.communicate(data)
292 self.wfile.write(stdout)
293 if stderr:
294 self.log_error('%s', stderr)
Brian Curtin3606f952010-11-05 15:12:47 +0000295 p.stderr.close()
296 p.stdout.close()
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000297 status = p.returncode
298 if status:
299 self.log_error("CGI script exit status %#x", status)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000300 else:
Guido van Rossumbcbdc952001-10-17 06:45:56 +0000301 self.log_message("CGI script exited OK")
Guido van Rossume7e578f1995-08-04 04:00:20 +0000302
303
Gregory P. Smith923ba362009-04-06 06:33:26 +0000304# TODO(gregory.p.smith): Move this into an appropriate library.
305def _url_collapse_path_split(path):
306 """
307 Given a URL path, remove extra '/'s and '.' path elements and collapse
308 any '..' references.
309
310 Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
311
312 Returns: A tuple of (head, tail) where tail is everything after the final /
313 and head is everything before it. Head will always start with a '/' and,
314 if it contains anything else, never have a trailing '/'.
315
316 Raises: IndexError if too many '..' occur within the path.
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 = []
321 for part in path.split('/'):
322 if part == '.':
323 path_parts.append('')
324 else:
325 path_parts.append(part)
326 # Filter out blank non trailing parts before consuming the '..'.
327 path_parts = [part for part in path_parts[:-1] if part] + path_parts[-1:]
328 if path_parts:
Senthil Kumaranfb2e8742012-04-11 03:07:57 +0800329 tail_part = path_parts.pop()
Gregory P. Smith923ba362009-04-06 06:33:26 +0000330 else:
331 tail_part = ''
332 head_parts = []
333 for part in path_parts:
334 if part == '..':
335 head_parts.pop()
336 else:
337 head_parts.append(part)
338 if tail_part and tail_part == '..':
339 head_parts.pop()
340 tail_part = ''
341 return ('/' + '/'.join(head_parts), tail_part)
342
343
Guido van Rossume7e578f1995-08-04 04:00:20 +0000344nobody = None
345
346def nobody_uid():
347 """Internal routine to get nobody's uid"""
348 global nobody
349 if nobody:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000350 return nobody
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000351 try:
352 import pwd
353 except ImportError:
354 return -1
Guido van Rossume7e578f1995-08-04 04:00:20 +0000355 try:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000356 nobody = pwd.getpwnam('nobody')[2]
Guido van Rossum630b8111999-04-28 12:21:47 +0000357 except KeyError:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000358 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
Guido van Rossume7e578f1995-08-04 04:00:20 +0000359 return nobody
360
361
362def executable(path):
363 """Test for executable file."""
364 try:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000365 st = os.stat(path)
Guido van Rossume7e578f1995-08-04 04:00:20 +0000366 except os.error:
Guido van Rossum8ca162f2002-04-07 06:36:23 +0000367 return False
Raymond Hettinger32200ae2002-06-01 19:51:15 +0000368 return st.st_mode & 0111 != 0
Guido van Rossume7e578f1995-08-04 04:00:20 +0000369
370
371def test(HandlerClass = CGIHTTPRequestHandler,
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000372 ServerClass = BaseHTTPServer.HTTPServer):
Guido van Rossume7e578f1995-08-04 04:00:20 +0000373 SimpleHTTPServer.test(HandlerClass, ServerClass)
374
375
376if __name__ == '__main__':
377 test()