This commit was manufactured by cvs2svn to create tag 'r23b1-mac'.
[python/dscho.git] / Lib / CGIHTTPServer.py
blobf14ebba7a39117127c79a2c1964734eae62c7719
1 """CGI-savvy HTTP Server.
3 This module builds on SimpleHTTPServer by implementing GET and POST
4 requests to cgi-bin scripts.
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.
11 In all cases, the implementation is intentionally naive -- all
12 requests are executed sychronously.
14 SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
15 -- it may execute arbitrary Python code or external programs.
17 """
20 __version__ = "0.4"
22 __all__ = ["CGIHTTPRequestHandler"]
24 import os
25 import sys
26 import urllib
27 import BaseHTTPServer
28 import SimpleHTTPServer
29 import select
32 class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
34 """Complete HTTP server with GET, HEAD and POST commands.
36 GET and HEAD also support running CGI scripts.
38 The POST command is *only* implemented for CGI scripts.
40 """
42 # Determine platform specifics
43 have_fork = hasattr(os, 'fork')
44 have_popen2 = hasattr(os, 'popen2')
45 have_popen3 = hasattr(os, 'popen3')
47 # Make rfile unbuffered -- we need to read one line and then pass
48 # the rest to a subprocess, so we can't use buffered input.
49 rbufsize = 0
51 def do_POST(self):
52 """Serve a POST request.
54 This is only implemented for CGI scripts.
56 """
58 if self.is_cgi():
59 self.run_cgi()
60 else:
61 self.send_error(501, "Can only POST to CGI scripts")
63 def send_head(self):
64 """Version of send_head that support CGI scripts"""
65 if self.is_cgi():
66 return self.run_cgi()
67 else:
68 return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
70 def is_cgi(self):
71 """Test whether self.path corresponds to a CGI script.
73 Return a tuple (dir, rest) if self.path requires running a
74 CGI script, None if not. Note that rest begins with a
75 slash if it is not empty.
77 The default implementation tests whether the path
78 begins with one of the strings in the list
79 self.cgi_directories (and the next character is a '/'
80 or the end of the string).
82 """
84 path = self.path
86 for x in self.cgi_directories:
87 i = len(x)
88 if path[:i] == x and (not path[i:] or path[i] == '/'):
89 self.cgi_info = path[:i], path[i+1:]
90 return True
91 return False
93 cgi_directories = ['/cgi-bin', '/htbin']
95 def is_executable(self, path):
96 """Test whether argument path is an executable file."""
97 return executable(path)
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")
104 def run_cgi(self):
105 """Execute a CGI script."""
106 dir, rest = self.cgi_info
107 i = rest.rfind('?')
108 if i >= 0:
109 rest, query = rest[:i], rest[i+1:]
110 else:
111 query = ''
112 i = rest.find('/')
113 if i >= 0:
114 script, rest = rest[:i], rest[i:]
115 else:
116 script, rest = rest, ''
117 scriptname = dir + '/' + script
118 scriptfile = self.translate_path(scriptname)
119 if not os.path.exists(scriptfile):
120 self.send_error(404, "No such CGI script (%s)" % `scriptname`)
121 return
122 if not os.path.isfile(scriptfile):
123 self.send_error(403, "CGI script is not a plain file (%s)" %
124 `scriptname`)
125 return
126 ispy = self.is_python(scriptname)
127 if not ispy:
128 if not (self.have_fork or self.have_popen2 or self.have_popen3):
129 self.send_error(403, "CGI script is not a Python script (%s)" %
130 `scriptname`)
131 return
132 if not self.is_executable(scriptfile):
133 self.send_error(403, "CGI script is not executable (%s)" %
134 `scriptname`)
135 return
137 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
138 # XXX Much of the following could be prepared ahead of time!
139 env = {}
140 env['SERVER_SOFTWARE'] = self.version_string()
141 env['SERVER_NAME'] = self.server.server_name
142 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
143 env['SERVER_PROTOCOL'] = self.protocol_version
144 env['SERVER_PORT'] = str(self.server.server_port)
145 env['REQUEST_METHOD'] = self.command
146 uqrest = urllib.unquote(rest)
147 env['PATH_INFO'] = uqrest
148 env['PATH_TRANSLATED'] = self.translate_path(uqrest)
149 env['SCRIPT_NAME'] = scriptname
150 if query:
151 env['QUERY_STRING'] = query
152 host = self.address_string()
153 if host != self.client_address[0]:
154 env['REMOTE_HOST'] = host
155 env['REMOTE_ADDR'] = self.client_address[0]
156 # XXX AUTH_TYPE
157 # XXX REMOTE_USER
158 # XXX REMOTE_IDENT
159 if self.headers.typeheader is None:
160 env['CONTENT_TYPE'] = self.headers.type
161 else:
162 env['CONTENT_TYPE'] = self.headers.typeheader
163 length = self.headers.getheader('content-length')
164 if length:
165 env['CONTENT_LENGTH'] = length
166 accept = []
167 for line in self.headers.getallmatchingheaders('accept'):
168 if line[:1] in "\t\n\r ":
169 accept.append(line.strip())
170 else:
171 accept = accept + line[7:].split(',')
172 env['HTTP_ACCEPT'] = ','.join(accept)
173 ua = self.headers.getheader('user-agent')
174 if ua:
175 env['HTTP_USER_AGENT'] = ua
176 co = filter(None, self.headers.getheaders('cookie'))
177 if co:
178 env['HTTP_COOKIE'] = ', '.join(co)
179 # XXX Other HTTP_* headers
180 if not self.have_fork:
181 # Since we're setting the env in the parent, provide empty
182 # values to override previously set values
183 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
184 'HTTP_USER_AGENT', 'HTTP_COOKIE'):
185 env.setdefault(k, "")
186 os.environ.update(env)
188 self.send_response(200, "Script output follows")
190 decoded_query = query.replace('+', ' ')
192 if self.have_fork:
193 # Unix -- fork as we should
194 args = [script]
195 if '=' not in decoded_query:
196 args.append(decoded_query)
197 nobody = nobody_uid()
198 self.wfile.flush() # Always flush before forking
199 pid = os.fork()
200 if pid != 0:
201 # Parent
202 pid, sts = os.waitpid(pid, 0)
203 # throw away additional data [see bug #427345]
204 while select.select([self.rfile], [], [], 0)[0]:
205 waste = self.rfile.read(1)
206 if sts:
207 self.log_error("CGI script exit status %#x", sts)
208 return
209 # Child
210 try:
211 try:
212 os.setuid(nobody)
213 except os.error:
214 pass
215 os.dup2(self.rfile.fileno(), 0)
216 os.dup2(self.wfile.fileno(), 1)
217 os.execve(scriptfile, args, env)
218 except:
219 self.server.handle_error(self.request, self.client_address)
220 os._exit(127)
222 elif self.have_popen2 or self.have_popen3:
223 # Windows -- use popen2 or popen3 to create a subprocess
224 import shutil
225 if self.have_popen3:
226 popenx = os.popen3
227 else:
228 popenx = os.popen2
229 cmdline = scriptfile
230 if self.is_python(scriptfile):
231 interp = sys.executable
232 if interp.lower().endswith("w.exe"):
233 # On Windows, use python.exe, not pythonw.exe
234 interp = interp[:-5] + interp[-4:]
235 cmdline = "%s -u %s" % (interp, cmdline)
236 if '=' not in query and '"' not in query:
237 cmdline = '%s "%s"' % (cmdline, query)
238 self.log_message("command: %s", cmdline)
239 try:
240 nbytes = int(length)
241 except (TypeError, ValueError):
242 nbytes = 0
243 files = popenx(cmdline, 'b')
244 fi = files[0]
245 fo = files[1]
246 if self.have_popen3:
247 fe = files[2]
248 if self.command.lower() == "post" and nbytes > 0:
249 data = self.rfile.read(nbytes)
250 fi.write(data)
251 # throw away additional data [see bug #427345]
252 while select.select([self.rfile._sock], [], [], 0)[0]:
253 waste = self.rfile._sock.recv(1)
254 fi.close()
255 shutil.copyfileobj(fo, self.wfile)
256 if self.have_popen3:
257 errors = fe.read()
258 fe.close()
259 if errors:
260 self.log_error('%s', errors)
261 sts = fo.close()
262 if sts:
263 self.log_error("CGI script exit status %#x", sts)
264 else:
265 self.log_message("CGI script exited OK")
267 else:
268 # Other O.S. -- execute script in this process
269 save_argv = sys.argv
270 save_stdin = sys.stdin
271 save_stdout = sys.stdout
272 save_stderr = sys.stderr
273 try:
274 try:
275 sys.argv = [scriptfile]
276 if '=' not in decoded_query:
277 sys.argv.append(decoded_query)
278 sys.stdout = self.wfile
279 sys.stdin = self.rfile
280 execfile(scriptfile, {"__name__": "__main__"})
281 finally:
282 sys.argv = save_argv
283 sys.stdin = save_stdin
284 sys.stdout = save_stdout
285 sys.stderr = save_stderr
286 except SystemExit, sts:
287 self.log_error("CGI script exit status %s", str(sts))
288 else:
289 self.log_message("CGI script exited OK")
292 nobody = None
294 def nobody_uid():
295 """Internal routine to get nobody's uid"""
296 global nobody
297 if nobody:
298 return nobody
299 try:
300 import pwd
301 except ImportError:
302 return -1
303 try:
304 nobody = pwd.getpwnam('nobody')[2]
305 except KeyError:
306 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
307 return nobody
310 def executable(path):
311 """Test for executable file."""
312 try:
313 st = os.stat(path)
314 except os.error:
315 return False
316 return st.st_mode & 0111 != 0
319 def test(HandlerClass = CGIHTTPRequestHandler,
320 ServerClass = BaseHTTPServer.HTTPServer):
321 SimpleHTTPServer.test(HandlerClass, ServerClass)
324 if __name__ == '__main__':
325 test()