blob: ed373483d9dafa9610ac342e726731ca62a44c52 [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)
88 if splitpath[0] in self.cgi_directories:
89 self.cgi_info = splitpath
90 return True
Tim Petersbc0e9102002-04-04 22:55:58 +000091 return False
Guido van Rossume7e578f1995-08-04 04:00:20 +000092
93 cgi_directories = ['/cgi-bin', '/htbin']
94
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000095 def is_executable(self, path):
96 """Test whether argument path is an executable file."""
97 return executable(path)
98
99 def is_python(self, path):
100 """Test whether argument path is a Python script."""
101 head, tail = os.path.splitext(path)
102 return tail.lower() in (".py", ".pyw")
103
Guido van Rossume7e578f1995-08-04 04:00:20 +0000104 def run_cgi(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000105 """Execute a CGI script."""
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000106 path = self.path
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000107 dir, rest = self.cgi_info
Tim Petersf733abb2007-01-30 03:03:46 +0000108
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000109 i = path.find('/', len(dir) + 1)
110 while i >= 0:
111 nextdir = path[:i]
112 nextrest = path[i+1:]
113
114 scriptdir = self.translate_path(nextdir)
115 if os.path.isdir(scriptdir):
116 dir, rest = nextdir, nextrest
117 i = path.find('/', len(dir) + 1)
118 else:
119 break
120
121 # find an explicit query string, if present.
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000122 i = rest.rfind('?')
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000123 if i >= 0:
124 rest, query = rest[:i], rest[i+1:]
125 else:
126 query = ''
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000127
128 # dissect the part after the directory name into a script name &
129 # a possible additional path, to be stored in PATH_INFO.
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000130 i = rest.find('/')
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000131 if i >= 0:
132 script, rest = rest[:i], rest[i:]
133 else:
134 script, rest = rest, ''
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000135
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000136 scriptname = dir + '/' + script
137 scriptfile = self.translate_path(scriptname)
138 if not os.path.exists(scriptfile):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000139 self.send_error(404, "No such CGI script (%r)" % scriptname)
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000140 return
141 if not os.path.isfile(scriptfile):
Tim Peters27f49612004-03-20 21:51:12 +0000142 self.send_error(403, "CGI script is not a plain file (%r)" %
Walter Dörwald70a6b492004-02-12 17:35:32 +0000143 scriptname)
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000144 return
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000145 ispy = self.is_python(scriptname)
146 if not ispy:
Guido van Rossum8cb65402002-02-01 16:27:59 +0000147 if not (self.have_fork or self.have_popen2 or self.have_popen3):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000148 self.send_error(403, "CGI script is not a Python script (%r)" %
149 scriptname)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000150 return
151 if not self.is_executable(scriptfile):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000152 self.send_error(403, "CGI script is not executable (%r)" %
153 scriptname)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000154 return
155
156 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
157 # XXX Much of the following could be prepared ahead of time!
Senthil Kumarana9bd0cc2010-10-03 18:16:52 +0000158 env = copy.deepcopy(os.environ)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000159 env['SERVER_SOFTWARE'] = self.version_string()
160 env['SERVER_NAME'] = self.server.server_name
161 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
162 env['SERVER_PROTOCOL'] = self.protocol_version
163 env['SERVER_PORT'] = str(self.server.server_port)
164 env['REQUEST_METHOD'] = self.command
165 uqrest = urllib.unquote(rest)
166 env['PATH_INFO'] = uqrest
167 env['PATH_TRANSLATED'] = self.translate_path(uqrest)
168 env['SCRIPT_NAME'] = scriptname
169 if query:
170 env['QUERY_STRING'] = query
171 host = self.address_string()
172 if host != self.client_address[0]:
173 env['REMOTE_HOST'] = host
174 env['REMOTE_ADDR'] = self.client_address[0]
Martin v. Löwisa28b3e62004-08-29 16:53:26 +0000175 authorization = self.headers.getheader("authorization")
176 if authorization:
177 authorization = authorization.split()
178 if len(authorization) == 2:
179 import base64, binascii
180 env['AUTH_TYPE'] = authorization[0]
181 if authorization[0].lower() == "basic":
182 try:
183 authorization = base64.decodestring(authorization[1])
184 except binascii.Error:
185 pass
186 else:
187 authorization = authorization.split(':')
188 if len(authorization) == 2:
189 env['REMOTE_USER'] = authorization[0]
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000190 # XXX REMOTE_IDENT
191 if self.headers.typeheader is None:
192 env['CONTENT_TYPE'] = self.headers.type
193 else:
194 env['CONTENT_TYPE'] = self.headers.typeheader
195 length = self.headers.getheader('content-length')
196 if length:
197 env['CONTENT_LENGTH'] = length
Collin Winter83b2bf62007-03-09 03:15:56 +0000198 referer = self.headers.getheader('referer')
199 if referer:
200 env['HTTP_REFERER'] = referer
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000201 accept = []
202 for line in self.headers.getallmatchingheaders('accept'):
Eric S. Raymond7e642e82001-02-09 12:10:26 +0000203 if line[:1] in "\t\n\r ":
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000204 accept.append(line.strip())
Guido van Rossum01fc65d1998-05-13 20:13:24 +0000205 else:
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000206 accept = accept + line[7:].split(',')
207 env['HTTP_ACCEPT'] = ','.join(accept)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000208 ua = self.headers.getheader('user-agent')
209 if ua:
210 env['HTTP_USER_AGENT'] = ua
211 co = filter(None, self.headers.getheaders('cookie'))
212 if co:
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000213 env['HTTP_COOKIE'] = ', '.join(co)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000214 # XXX Other HTTP_* headers
Guido van Rossum70ec0b42004-03-20 22:18:03 +0000215 # Since we're setting the env in the parent, provide empty
216 # values to override previously set values
217 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
Collin Winter83b2bf62007-03-09 03:15:56 +0000218 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
Guido van Rossum70ec0b42004-03-20 22:18:03 +0000219 env.setdefault(k, "")
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000220
221 self.send_response(200, "Script output follows")
222
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000223 decoded_query = query.replace('+', ' ')
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000224
225 if self.have_fork:
226 # Unix -- fork as we should
227 args = [script]
228 if '=' not in decoded_query:
229 args.append(decoded_query)
230 nobody = nobody_uid()
231 self.wfile.flush() # Always flush before forking
232 pid = os.fork()
233 if pid != 0:
234 # Parent
235 pid, sts = os.waitpid(pid, 0)
Steve Holden8a978f72003-01-08 18:53:18 +0000236 # throw away additional data [see bug #427345]
237 while select.select([self.rfile], [], [], 0)[0]:
Raymond Hettingere2f18372003-06-29 05:06:56 +0000238 if not self.rfile.read(1):
239 break
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000240 if sts:
241 self.log_error("CGI script exit status %#x", sts)
242 return
243 # Child
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000244 try:
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000245 try:
246 os.setuid(nobody)
247 except os.error:
248 pass
249 os.dup2(self.rfile.fileno(), 0)
250 os.dup2(self.wfile.fileno(), 1)
Senthil Kumarana9bd0cc2010-10-03 18:16:52 +0000251 os.execve(scriptfile, args, env)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000252 except:
253 self.server.handle_error(self.request, self.client_address)
254 os._exit(127)
255
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000256 else:
257 # Non Unix - use subprocess
258 import subprocess
259 cmdline = [scriptfile]
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000260 if self.is_python(scriptfile):
261 interp = sys.executable
262 if interp.lower().endswith("w.exe"):
Guido van Rossum0afde132001-10-26 03:38:46 +0000263 # On Windows, use python.exe, not pythonw.exe
264 interp = interp[:-5] + interp[-4:]
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000265 cmdline = [interp, '-u'] + cmdline
266 if '=' not in query:
267 cmdline.append(query)
268
269 self.log_message("command: %s", subprocess.list2cmdline(cmdline))
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000270 try:
271 nbytes = int(length)
Guido van Rossumb3903152002-10-17 16:21:35 +0000272 except (TypeError, ValueError):
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000273 nbytes = 0
Senthil Kumaran5dff3542009-11-11 17:22:35 +0000274 p = subprocess.Popen(cmdline,
275 stdin = subprocess.PIPE,
276 stdout = subprocess.PIPE,
Senthil Kumarana9bd0cc2010-10-03 18:16:52 +0000277 stderr = subprocess.PIPE,
278 env = env
Senthil Kumaran5dff3542009-11-11 17:22:35 +0000279 )
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000280 if self.command.lower() == "post" and nbytes > 0:
281 data = self.rfile.read(nbytes)
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000282 else:
283 data = None
Steve Holden8a978f72003-01-08 18:53:18 +0000284 # throw away additional data [see bug #427345]
285 while select.select([self.rfile._sock], [], [], 0)[0]:
Raymond Hettingere2f18372003-06-29 05:06:56 +0000286 if not self.rfile._sock.recv(1):
287 break
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000288 stdout, stderr = p.communicate(data)
289 self.wfile.write(stdout)
290 if stderr:
291 self.log_error('%s', stderr)
292 status = p.returncode
293 if status:
294 self.log_error("CGI script exit status %#x", status)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000295 else:
Guido van Rossumbcbdc952001-10-17 06:45:56 +0000296 self.log_message("CGI script exited OK")
Guido van Rossume7e578f1995-08-04 04:00:20 +0000297
298
Gregory P. Smith923ba362009-04-06 06:33:26 +0000299# TODO(gregory.p.smith): Move this into an appropriate library.
300def _url_collapse_path_split(path):
301 """
302 Given a URL path, remove extra '/'s and '.' path elements and collapse
303 any '..' references.
304
305 Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
306
307 Returns: A tuple of (head, tail) where tail is everything after the final /
308 and head is everything before it. Head will always start with a '/' and,
309 if it contains anything else, never have a trailing '/'.
310
311 Raises: IndexError if too many '..' occur within the path.
312 """
313 # Similar to os.path.split(os.path.normpath(path)) but specific to URL
314 # path semantics rather than local operating system semantics.
315 path_parts = []
316 for part in path.split('/'):
317 if part == '.':
318 path_parts.append('')
319 else:
320 path_parts.append(part)
321 # Filter out blank non trailing parts before consuming the '..'.
322 path_parts = [part for part in path_parts[:-1] if part] + path_parts[-1:]
323 if path_parts:
324 tail_part = path_parts.pop()
325 else:
326 tail_part = ''
327 head_parts = []
328 for part in path_parts:
329 if part == '..':
330 head_parts.pop()
331 else:
332 head_parts.append(part)
333 if tail_part and tail_part == '..':
334 head_parts.pop()
335 tail_part = ''
336 return ('/' + '/'.join(head_parts), tail_part)
337
338
Guido van Rossume7e578f1995-08-04 04:00:20 +0000339nobody = None
340
341def nobody_uid():
342 """Internal routine to get nobody's uid"""
343 global nobody
344 if nobody:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000345 return nobody
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000346 try:
347 import pwd
348 except ImportError:
349 return -1
Guido van Rossume7e578f1995-08-04 04:00:20 +0000350 try:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000351 nobody = pwd.getpwnam('nobody')[2]
Guido van Rossum630b8111999-04-28 12:21:47 +0000352 except KeyError:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000353 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
Guido van Rossume7e578f1995-08-04 04:00:20 +0000354 return nobody
355
356
357def executable(path):
358 """Test for executable file."""
359 try:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000360 st = os.stat(path)
Guido van Rossume7e578f1995-08-04 04:00:20 +0000361 except os.error:
Guido van Rossum8ca162f2002-04-07 06:36:23 +0000362 return False
Raymond Hettinger32200ae2002-06-01 19:51:15 +0000363 return st.st_mode & 0111 != 0
Guido van Rossume7e578f1995-08-04 04:00:20 +0000364
365
366def test(HandlerClass = CGIHTTPRequestHandler,
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000367 ServerClass = BaseHTTPServer.HTTPServer):
Guido van Rossume7e578f1995-08-04 04:00:20 +0000368 SimpleHTTPServer.test(HandlerClass, ServerClass)
369
370
371if __name__ == '__main__':
372 test()