blob: 88613ad200018b2cd6000b146091d9648cc0371a [file] [log] [blame]
Tor Norbye3a2425a52013-11-04 10:16:08 -08001"""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
32
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
44 # Determine platform specifics
45 have_fork = hasattr(os, 'fork')
46 have_popen2 = hasattr(os, 'popen2')
47 have_popen3 = hasattr(os, 'popen3')
48
49 # 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
53 def do_POST(self):
54 """Serve a POST request.
55
56 This is only implemented for CGI scripts.
57
58 """
59
60 if self.is_cgi():
61 self.run_cgi()
62 else:
63 self.send_error(501, "Can only POST to CGI scripts")
64
65 def send_head(self):
66 """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)
71
72 def is_cgi(self):
73 """Test whether self.path corresponds to a CGI script.
74
75 Return a tuple (dir, rest) if self.path requires running a
76 CGI script, None if not. Note that rest begins with a
77 slash if it is not empty.
78
79 The default implementation tests whether the path
80 begins with one of the strings in the list
81 self.cgi_directories (and the next character is a '/'
82 or the end of the string).
83
84 """
85
86 path = self.path
87
88 for x in self.cgi_directories:
89 i = len(x)
90 if path[:i] == x and (not path[i:] or path[i] == '/'):
91 self.cgi_info = path[:i], path[i+1:]
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 path = self.path
109 dir, rest = self.cgi_info
110
111 i = path.find('/', len(dir) + 1)
112 while i >= 0:
113 nextdir = path[:i]
114 nextrest = path[i+1:]
115
116 scriptdir = self.translate_path(nextdir)
117 if os.path.isdir(scriptdir):
118 dir, rest = nextdir, nextrest
119 i = path.find('/', len(dir) + 1)
120 else:
121 break
122
123 # find an explicit query string, if present.
124 i = rest.rfind('?')
125 if i >= 0:
126 rest, query = rest[:i], rest[i+1:]
127 else:
128 query = ''
129
130 # dissect the part after the directory name into a script name &
131 # a possible additional path, to be stored in PATH_INFO.
132 i = rest.find('/')
133 if i >= 0:
134 script, rest = rest[:i], rest[i:]
135 else:
136 script, rest = rest, ''
137
138 scriptname = dir + '/' + script
139 scriptfile = self.translate_path(scriptname)
140 if not os.path.exists(scriptfile):
141 self.send_error(404, "No such CGI script (%r)" % scriptname)
142 return
143 if not os.path.isfile(scriptfile):
144 self.send_error(403, "CGI script is not a plain file (%r)" %
145 scriptname)
146 return
147 ispy = self.is_python(scriptname)
148 if not ispy:
149 if not (self.have_fork or self.have_popen2 or self.have_popen3):
150 self.send_error(403, "CGI script is not a Python script (%r)" %
151 scriptname)
152 return
153 if not self.is_executable(scriptfile):
154 self.send_error(403, "CGI script is not executable (%r)" %
155 scriptname)
156 return
157
158 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
159 # XXX Much of the following could be prepared ahead of time!
160 env = {}
161 env['SERVER_SOFTWARE'] = self.version_string()
162 env['SERVER_NAME'] = self.server.server_name
163 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
164 env['SERVER_PROTOCOL'] = self.protocol_version
165 env['SERVER_PORT'] = str(self.server.server_port)
166 env['REQUEST_METHOD'] = self.command
167 uqrest = urllib.unquote(rest)
168 env['PATH_INFO'] = uqrest
169 env['PATH_TRANSLATED'] = self.translate_path(uqrest)
170 env['SCRIPT_NAME'] = scriptname
171 if query:
172 env['QUERY_STRING'] = query
173 host = self.address_string()
174 if host != self.client_address[0]:
175 env['REMOTE_HOST'] = host
176 env['REMOTE_ADDR'] = self.client_address[0]
177 authorization = self.headers.getheader("authorization")
178 if authorization:
179 authorization = authorization.split()
180 if len(authorization) == 2:
181 import base64, binascii
182 env['AUTH_TYPE'] = authorization[0]
183 if authorization[0].lower() == "basic":
184 try:
185 authorization = base64.decodestring(authorization[1])
186 except binascii.Error:
187 pass
188 else:
189 authorization = authorization.split(':')
190 if len(authorization) == 2:
191 env['REMOTE_USER'] = authorization[0]
192 # XXX REMOTE_IDENT
193 if self.headers.typeheader is None:
194 env['CONTENT_TYPE'] = self.headers.type
195 else:
196 env['CONTENT_TYPE'] = self.headers.typeheader
197 length = self.headers.getheader('content-length')
198 if length:
199 env['CONTENT_LENGTH'] = length
200 accept = []
201 for line in self.headers.getallmatchingheaders('accept'):
202 if line[:1] in "\t\n\r ":
203 accept.append(line.strip())
204 else:
205 accept = accept + line[7:].split(',')
206 env['HTTP_ACCEPT'] = ','.join(accept)
207 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:
212 env['HTTP_COOKIE'] = ', '.join(co)
213 # XXX Other HTTP_* headers
214 # 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',
217 'HTTP_USER_AGENT', 'HTTP_COOKIE'):
218 env.setdefault(k, "")
219 os.environ.update(env)
220
221 self.send_response(200, "Script output follows")
222
223 decoded_query = query.replace('+', ' ')
224
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)
236 # throw away additional data [see bug #427345]
237 while select.select([self.rfile], [], [], 0)[0]:
238 if not self.rfile.read(1):
239 break
240 if sts:
241 self.log_error("CGI script exit status %#x", sts)
242 return
243 # Child
244 try:
245 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)
251 os.execve(scriptfile, args, os.environ)
252 except:
253 self.server.handle_error(self.request, self.client_address)
254 os._exit(127)
255
256 elif self.have_popen2 or self.have_popen3:
257 # Windows -- use popen2 or popen3 to create a subprocess
258 import shutil
259 if self.have_popen3:
260 popenx = os.popen3
261 else:
262 popenx = os.popen2
263 cmdline = scriptfile
264 if self.is_python(scriptfile):
265 interp = sys.executable
266 if interp.lower().endswith("w.exe"):
267 # On Windows, use python.exe, not pythonw.exe
268 interp = interp[:-5] + interp[-4:]
269 cmdline = "%s -u %s" % (interp, cmdline)
270 if '=' not in query and '"' not in query:
271 cmdline = '%s "%s"' % (cmdline, query)
272 self.log_message("command: %s", cmdline)
273 try:
274 nbytes = int(length)
275 except (TypeError, ValueError):
276 nbytes = 0
277 files = popenx(cmdline, 'b')
278 fi = files[0]
279 fo = files[1]
280 if self.have_popen3:
281 fe = files[2]
282 if self.command.lower() == "post" and nbytes > 0:
283 data = self.rfile.read(nbytes)
284 fi.write(data)
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 fi.close()
290 shutil.copyfileobj(fo, self.wfile)
291 if self.have_popen3:
292 errors = fe.read()
293 fe.close()
294 if errors:
295 self.log_error('%s', errors)
296 sts = fo.close()
297 if sts:
298 self.log_error("CGI script exit status %#x", sts)
299 else:
300 self.log_message("CGI script exited OK")
301
302 else:
303 # Other O.S. -- execute script in this process
304 save_argv = sys.argv
305 save_stdin = sys.stdin
306 save_stdout = sys.stdout
307 save_stderr = sys.stderr
308 try:
309 save_cwd = os.getcwd()
310 try:
311 sys.argv = [scriptfile]
312 if '=' not in decoded_query:
313 sys.argv.append(decoded_query)
314 sys.stdout = self.wfile
315 sys.stdin = self.rfile
316 execfile(scriptfile, {"__name__": "__main__"})
317 finally:
318 sys.argv = save_argv
319 sys.stdin = save_stdin
320 sys.stdout = save_stdout
321 sys.stderr = save_stderr
322 os.chdir(save_cwd)
323 except SystemExit, sts:
324 self.log_error("CGI script exit status %s", str(sts))
325 else:
326 self.log_message("CGI script exited OK")
327
328
329nobody = None
330
331def nobody_uid():
332 """Internal routine to get nobody's uid"""
333 global nobody
334 if nobody:
335 return nobody
336 try:
337 import pwd
338 except ImportError:
339 return -1
340 try:
341 nobody = pwd.getpwnam('nobody')[2]
342 except KeyError:
343 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
344 return nobody
345
346
347def executable(path):
348 """Test for executable file."""
349 try:
350 st = os.stat(path)
351 except os.error:
352 return False
353 return st.st_mode & 0111 != 0
354
355
356def test(HandlerClass = CGIHTTPRequestHandler,
357 ServerClass = BaseHTTPServer.HTTPServer):
358 SimpleHTTPServer.test(HandlerClass, ServerClass)
359
360
361if __name__ == '__main__':
362 test()