]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/CGIHTTPServer.py
make python subprocess module work with ape/sh
[plan9front.git] / sys / lib / python / CGIHTTPServer.py
1 """CGI-savvy HTTP Server.
2
3 This module builds on SimpleHTTPServer by implementing GET and POST
4 requests to cgi-bin scripts.
5
6 If the os.fork() function is not present (e.g. on Windows),
7 os.popen2() is used as a fallback, with slightly altered semantics; if
8 that function is not present either (e.g. on Macintosh), only Python
9 scripts are supported, and they are executed by the current process.
10
11 In all cases, the implementation is intentionally naive -- all
12 requests are executed sychronously.
13
14 SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
15 -- it may execute arbitrary Python code or external programs.
16
17 Note that status code 200 is sent prior to execution of a CGI script, so
18 scripts cannot send other status codes such as 302 (redirect).
19 """
20
21
22 __version__ = "0.4"
23
24 __all__ = ["CGIHTTPRequestHandler"]
25
26 import os
27 import sys
28 import urllib
29 import BaseHTTPServer
30 import SimpleHTTPServer
31 import select
32
33
34 class 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
329 nobody = None
330
331 def 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
347 def 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
356 def test(HandlerClass = CGIHTTPRequestHandler,
357          ServerClass = BaseHTTPServer.HTTPServer):
358     SimpleHTTPServer.test(HandlerClass, ServerClass)
359
360
361 if __name__ == '__main__':
362     test()