blob: e8494a4ba26aa9323b0e100e1fcbb7de825eb5e3 [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
Guido van Rossume7e578f1995-08-04 04:00:20 +000032
33
34class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
35
36 """Complete HTTP server with GET, HEAD and POST commands.
37
38 GET and HEAD also support running CGI scripts.
39
40 The POST command is *only* implemented for CGI scripts.
41
42 """
43
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000044 # Determine platform specifics
45 have_fork = hasattr(os, 'fork')
46 have_popen2 = hasattr(os, 'popen2')
Guido van Rossum8cb65402002-02-01 16:27:59 +000047 have_popen3 = hasattr(os, 'popen3')
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000048
Guido van Rossum6aefd912000-09-01 03:27:34 +000049 # Make rfile unbuffered -- we need to read one line and then pass
50 # the rest to a subprocess, so we can't use buffered input.
51 rbufsize = 0
52
Guido van Rossume7e578f1995-08-04 04:00:20 +000053 def do_POST(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000054 """Serve a POST request.
Guido van Rossume7e578f1995-08-04 04:00:20 +000055
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000056 This is only implemented for CGI scripts.
Guido van Rossume7e578f1995-08-04 04:00:20 +000057
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000058 """
Guido van Rossume7e578f1995-08-04 04:00:20 +000059
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000060 if self.is_cgi():
61 self.run_cgi()
62 else:
63 self.send_error(501, "Can only POST to CGI scripts")
Guido van Rossume7e578f1995-08-04 04:00:20 +000064
65 def send_head(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000066 """Version of send_head that support CGI scripts"""
67 if self.is_cgi():
68 return self.run_cgi()
69 else:
70 return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
Guido van Rossume7e578f1995-08-04 04:00:20 +000071
72 def is_cgi(self):
Gregory P. Smith923ba362009-04-06 06:33:26 +000073 """Test whether self.path corresponds to a CGI script.
Guido van Rossume7e578f1995-08-04 04:00:20 +000074
Gregory P. Smith923ba362009-04-06 06:33:26 +000075 Returns True and updates the cgi_info attribute to the tuple
76 (dir, rest) if self.path requires running a CGI script.
77 Returns False otherwise.
Guido van Rossume7e578f1995-08-04 04:00:20 +000078
Gregory P. Smith4bd76642009-05-03 20:27:25 +000079 If any exception is raised, the caller should assume that
80 self.path was rejected as invalid and act accordingly.
81
Gregory P. Smith923ba362009-04-06 06:33:26 +000082 The default implementation tests whether the normalized url
83 path begins with one of the strings in self.cgi_directories
84 (and the next character is a '/' or the end of the string).
Guido van Rossum45e2fbc1998-03-26 21:13:24 +000085 """
Gregory P. Smith923ba362009-04-06 06:33:26 +000086 splitpath = _url_collapse_path_split(self.path)
87 if splitpath[0] in self.cgi_directories:
88 self.cgi_info = splitpath
89 return True
Tim Petersbc0e9102002-04-04 22:55:58 +000090 return False
Guido van Rossume7e578f1995-08-04 04:00:20 +000091
92 cgi_directories = ['/cgi-bin', '/htbin']
93
Guido van Rossume7d6b0a2000-09-19 04:01:01 +000094 def is_executable(self, path):
95 """Test whether argument path is an executable file."""
96 return executable(path)
97
98 def is_python(self, path):
99 """Test whether argument path is a Python script."""
100 head, tail = os.path.splitext(path)
101 return tail.lower() in (".py", ".pyw")
102
Guido van Rossume7e578f1995-08-04 04:00:20 +0000103 def run_cgi(self):
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000104 """Execute a CGI script."""
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000105 path = self.path
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000106 dir, rest = self.cgi_info
Tim Petersf733abb2007-01-30 03:03:46 +0000107
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000108 i = path.find('/', len(dir) + 1)
109 while i >= 0:
110 nextdir = path[:i]
111 nextrest = path[i+1:]
112
113 scriptdir = self.translate_path(nextdir)
114 if os.path.isdir(scriptdir):
115 dir, rest = nextdir, nextrest
116 i = path.find('/', len(dir) + 1)
117 else:
118 break
119
120 # find an explicit query string, if present.
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000121 i = rest.rfind('?')
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000122 if i >= 0:
123 rest, query = rest[:i], rest[i+1:]
124 else:
125 query = ''
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000126
127 # dissect the part after the directory name into a script name &
128 # a possible additional path, to be stored in PATH_INFO.
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000129 i = rest.find('/')
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000130 if i >= 0:
131 script, rest = rest[:i], rest[i:]
132 else:
133 script, rest = rest, ''
Andrew M. Kuchlingb29069d2006-12-22 13:25:02 +0000134
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000135 scriptname = dir + '/' + script
136 scriptfile = self.translate_path(scriptname)
137 if not os.path.exists(scriptfile):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000138 self.send_error(404, "No such CGI script (%r)" % scriptname)
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000139 return
140 if not os.path.isfile(scriptfile):
Tim Peters27f49612004-03-20 21:51:12 +0000141 self.send_error(403, "CGI script is not a plain file (%r)" %
Walter Dörwald70a6b492004-02-12 17:35:32 +0000142 scriptname)
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000143 return
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000144 ispy = self.is_python(scriptname)
145 if not ispy:
Guido van Rossum8cb65402002-02-01 16:27:59 +0000146 if not (self.have_fork or self.have_popen2 or self.have_popen3):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000147 self.send_error(403, "CGI script is not a Python script (%r)" %
148 scriptname)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000149 return
150 if not self.is_executable(scriptfile):
Walter Dörwald70a6b492004-02-12 17:35:32 +0000151 self.send_error(403, "CGI script is not executable (%r)" %
152 scriptname)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000153 return
154
155 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
156 # XXX Much of the following could be prepared ahead of time!
157 env = {}
158 env['SERVER_SOFTWARE'] = self.version_string()
159 env['SERVER_NAME'] = self.server.server_name
160 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
161 env['SERVER_PROTOCOL'] = self.protocol_version
162 env['SERVER_PORT'] = str(self.server.server_port)
163 env['REQUEST_METHOD'] = self.command
164 uqrest = urllib.unquote(rest)
165 env['PATH_INFO'] = uqrest
166 env['PATH_TRANSLATED'] = self.translate_path(uqrest)
167 env['SCRIPT_NAME'] = scriptname
168 if query:
169 env['QUERY_STRING'] = query
170 host = self.address_string()
171 if host != self.client_address[0]:
172 env['REMOTE_HOST'] = host
173 env['REMOTE_ADDR'] = self.client_address[0]
Martin v. Löwisa28b3e62004-08-29 16:53:26 +0000174 authorization = self.headers.getheader("authorization")
175 if authorization:
176 authorization = authorization.split()
177 if len(authorization) == 2:
178 import base64, binascii
179 env['AUTH_TYPE'] = authorization[0]
180 if authorization[0].lower() == "basic":
181 try:
182 authorization = base64.decodestring(authorization[1])
183 except binascii.Error:
184 pass
185 else:
186 authorization = authorization.split(':')
187 if len(authorization) == 2:
188 env['REMOTE_USER'] = authorization[0]
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000189 # XXX REMOTE_IDENT
190 if self.headers.typeheader is None:
191 env['CONTENT_TYPE'] = self.headers.type
192 else:
193 env['CONTENT_TYPE'] = self.headers.typeheader
194 length = self.headers.getheader('content-length')
195 if length:
196 env['CONTENT_LENGTH'] = length
Collin Winter83b2bf62007-03-09 03:15:56 +0000197 referer = self.headers.getheader('referer')
198 if referer:
199 env['HTTP_REFERER'] = referer
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000200 accept = []
201 for line in self.headers.getallmatchingheaders('accept'):
Eric S. Raymond7e642e82001-02-09 12:10:26 +0000202 if line[:1] in "\t\n\r ":
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000203 accept.append(line.strip())
Guido van Rossum01fc65d1998-05-13 20:13:24 +0000204 else:
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000205 accept = accept + line[7:].split(',')
206 env['HTTP_ACCEPT'] = ','.join(accept)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000207 ua = self.headers.getheader('user-agent')
208 if ua:
209 env['HTTP_USER_AGENT'] = ua
210 co = filter(None, self.headers.getheaders('cookie'))
211 if co:
Eric S. Raymond6b71e742001-02-09 08:56:30 +0000212 env['HTTP_COOKIE'] = ', '.join(co)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000213 # XXX Other HTTP_* headers
Guido van Rossum70ec0b42004-03-20 22:18:03 +0000214 # Since we're setting the env in the parent, provide empty
215 # values to override previously set values
216 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
Collin Winter83b2bf62007-03-09 03:15:56 +0000217 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
Guido van Rossum70ec0b42004-03-20 22:18:03 +0000218 env.setdefault(k, "")
Guido van Rossume3ec2962002-08-20 20:07:10 +0000219 os.environ.update(env)
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)
Raymond Hettinger92f200b2003-07-14 06:56:32 +0000251 os.execve(scriptfile, args, os.environ)
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,
277 stderr = subprocess.PIPE
278 )
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000279 if self.command.lower() == "post" and nbytes > 0:
280 data = self.rfile.read(nbytes)
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000281 else:
282 data = None
Steve Holden8a978f72003-01-08 18:53:18 +0000283 # throw away additional data [see bug #427345]
284 while select.select([self.rfile._sock], [], [], 0)[0]:
Raymond Hettingere2f18372003-06-29 05:06:56 +0000285 if not self.rfile._sock.recv(1):
286 break
Senthil Kumaran3a145a12009-11-11 01:34:44 +0000287 stdout, stderr = p.communicate(data)
288 self.wfile.write(stdout)
289 if stderr:
290 self.log_error('%s', stderr)
291 status = p.returncode
292 if status:
293 self.log_error("CGI script exit status %#x", status)
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000294 else:
Guido van Rossumbcbdc952001-10-17 06:45:56 +0000295 self.log_message("CGI script exited OK")
Guido van Rossume7e578f1995-08-04 04:00:20 +0000296
297
Gregory P. Smith923ba362009-04-06 06:33:26 +0000298# TODO(gregory.p.smith): Move this into an appropriate library.
299def _url_collapse_path_split(path):
300 """
301 Given a URL path, remove extra '/'s and '.' path elements and collapse
302 any '..' references.
303
304 Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
305
306 Returns: A tuple of (head, tail) where tail is everything after the final /
307 and head is everything before it. Head will always start with a '/' and,
308 if it contains anything else, never have a trailing '/'.
309
310 Raises: IndexError if too many '..' occur within the path.
311 """
312 # Similar to os.path.split(os.path.normpath(path)) but specific to URL
313 # path semantics rather than local operating system semantics.
314 path_parts = []
315 for part in path.split('/'):
316 if part == '.':
317 path_parts.append('')
318 else:
319 path_parts.append(part)
320 # Filter out blank non trailing parts before consuming the '..'.
321 path_parts = [part for part in path_parts[:-1] if part] + path_parts[-1:]
322 if path_parts:
323 tail_part = path_parts.pop()
324 else:
325 tail_part = ''
326 head_parts = []
327 for part in path_parts:
328 if part == '..':
329 head_parts.pop()
330 else:
331 head_parts.append(part)
332 if tail_part and tail_part == '..':
333 head_parts.pop()
334 tail_part = ''
335 return ('/' + '/'.join(head_parts), tail_part)
336
337
Guido van Rossume7e578f1995-08-04 04:00:20 +0000338nobody = None
339
340def nobody_uid():
341 """Internal routine to get nobody's uid"""
342 global nobody
343 if nobody:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000344 return nobody
Guido van Rossume7d6b0a2000-09-19 04:01:01 +0000345 try:
346 import pwd
347 except ImportError:
348 return -1
Guido van Rossume7e578f1995-08-04 04:00:20 +0000349 try:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000350 nobody = pwd.getpwnam('nobody')[2]
Guido van Rossum630b8111999-04-28 12:21:47 +0000351 except KeyError:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000352 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
Guido van Rossume7e578f1995-08-04 04:00:20 +0000353 return nobody
354
355
356def executable(path):
357 """Test for executable file."""
358 try:
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000359 st = os.stat(path)
Guido van Rossume7e578f1995-08-04 04:00:20 +0000360 except os.error:
Guido van Rossum8ca162f2002-04-07 06:36:23 +0000361 return False
Raymond Hettinger32200ae2002-06-01 19:51:15 +0000362 return st.st_mode & 0111 != 0
Guido van Rossume7e578f1995-08-04 04:00:20 +0000363
364
365def test(HandlerClass = CGIHTTPRequestHandler,
Guido van Rossum45e2fbc1998-03-26 21:13:24 +0000366 ServerClass = BaseHTTPServer.HTTPServer):
Guido van Rossume7e578f1995-08-04 04:00:20 +0000367 SimpleHTTPServer.test(HandlerClass, ServerClass)
368
369
370if __name__ == '__main__':
371 test()