blob: ba1e76bb3eb21ec7fed2174677b2f050829c2d04 [file] [log] [blame]
Guido van Rossum5c971671996-07-22 15:23:25 +00001"""CGI-savvy HTTP Server.
2
3This module builds on SimpleHTTPServer by implementing GET and POST
4requests to cgi-bin scripts.
5
Jeremy Hyltond635b1d2000-09-26 17:32:27 +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.
Guido van Rossumaad67612000-05-08 17:31:04 +000016
Guido van Rossum5c971671996-07-22 15:23:25 +000017"""
18
19
Jeremy Hyltond635b1d2000-09-26 17:32:27 +000020__version__ = "0.4"
Guido van Rossum5c971671996-07-22 15:23:25 +000021
22
23import os
Jeremy Hyltond635b1d2000-09-26 17:32:27 +000024import sys
Guido van Rossum5c971671996-07-22 15:23:25 +000025import string
26import urllib
27import BaseHTTPServer
28import SimpleHTTPServer
29
30
31class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
32
33 """Complete HTTP server with GET, HEAD and POST commands.
34
35 GET and HEAD also support running CGI scripts.
36
37 The POST command is *only* implemented for CGI scripts.
38
39 """
40
Jeremy Hyltond635b1d2000-09-26 17:32:27 +000041 # Determine platform specifics
42 have_fork = hasattr(os, 'fork')
43 have_popen2 = hasattr(os, 'popen2')
44
Guido van Rossum8d691c82000-09-01 19:25:51 +000045 # Make rfile unbuffered -- we need to read one line and then pass
46 # the rest to a subprocess, so we can't use buffered input.
47 rbufsize = 0
48
Guido van Rossum5c971671996-07-22 15:23:25 +000049 def do_POST(self):
Guido van Rossum548703a1998-03-26 22:14:20 +000050 """Serve a POST request.
Guido van Rossum5c971671996-07-22 15:23:25 +000051
Guido van Rossum548703a1998-03-26 22:14:20 +000052 This is only implemented for CGI scripts.
Guido van Rossum5c971671996-07-22 15:23:25 +000053
Guido van Rossum548703a1998-03-26 22:14:20 +000054 """
Guido van Rossum5c971671996-07-22 15:23:25 +000055
Guido van Rossum548703a1998-03-26 22:14:20 +000056 if self.is_cgi():
57 self.run_cgi()
58 else:
59 self.send_error(501, "Can only POST to CGI scripts")
Guido van Rossum5c971671996-07-22 15:23:25 +000060
61 def send_head(self):
Guido van Rossum548703a1998-03-26 22:14:20 +000062 """Version of send_head that support CGI scripts"""
63 if self.is_cgi():
64 return self.run_cgi()
65 else:
66 return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
Guido van Rossum5c971671996-07-22 15:23:25 +000067
68 def is_cgi(self):
Jeremy Hyltond635b1d2000-09-26 17:32:27 +000069 """Test whether self.path corresponds to a CGI script.
Guido van Rossum5c971671996-07-22 15:23:25 +000070
Jeremy Hyltond635b1d2000-09-26 17:32:27 +000071 Return a tuple (dir, rest) if self.path requires running a
Guido van Rossum548703a1998-03-26 22:14:20 +000072 CGI script, None if not. Note that rest begins with a
73 slash if it is not empty.
Guido van Rossum5c971671996-07-22 15:23:25 +000074
Guido van Rossum548703a1998-03-26 22:14:20 +000075 The default implementation tests whether the path
76 begins with one of the strings in the list
77 self.cgi_directories (and the next character is a '/'
78 or the end of the string).
Guido van Rossum5c971671996-07-22 15:23:25 +000079
Guido van Rossum548703a1998-03-26 22:14:20 +000080 """
Guido van Rossum5c971671996-07-22 15:23:25 +000081
Guido van Rossum548703a1998-03-26 22:14:20 +000082 path = self.path
Guido van Rossum5c971671996-07-22 15:23:25 +000083
Guido van Rossum548703a1998-03-26 22:14:20 +000084 for x in self.cgi_directories:
85 i = len(x)
86 if path[:i] == x and (not path[i:] or path[i] == '/'):
87 self.cgi_info = path[:i], path[i+1:]
88 return 1
89 return 0
Guido van Rossum5c971671996-07-22 15:23:25 +000090
91 cgi_directories = ['/cgi-bin', '/htbin']
92
Jeremy Hyltond635b1d2000-09-26 17:32:27 +000093 def is_executable(self, path):
94 """Test whether argument path is an executable file."""
95 return executable(path)
96
97 def is_python(self, path):
98 """Test whether argument path is a Python script."""
99 head, tail = os.path.splitext(path)
100 return tail.lower() in (".py", ".pyw")
101
Guido van Rossum5c971671996-07-22 15:23:25 +0000102 def run_cgi(self):
Guido van Rossum548703a1998-03-26 22:14:20 +0000103 """Execute a CGI script."""
104 dir, rest = self.cgi_info
105 i = string.rfind(rest, '?')
106 if i >= 0:
107 rest, query = rest[:i], rest[i+1:]
108 else:
109 query = ''
110 i = string.find(rest, '/')
111 if i >= 0:
112 script, rest = rest[:i], rest[i:]
113 else:
114 script, rest = rest, ''
115 scriptname = dir + '/' + script
116 scriptfile = self.translate_path(scriptname)
117 if not os.path.exists(scriptfile):
118 self.send_error(404, "No such CGI script (%s)" % `scriptname`)
119 return
120 if not os.path.isfile(scriptfile):
121 self.send_error(403, "CGI script is not a plain file (%s)" %
122 `scriptname`)
123 return
Jeremy Hyltond635b1d2000-09-26 17:32:27 +0000124 ispy = self.is_python(scriptname)
125 if not ispy:
126 if not (self.have_fork or self.have_popen2):
127 self.send_error(403, "CGI script is not a Python script (%s)" %
128 `scriptname`)
129 return
130 if not self.is_executable(scriptfile):
131 self.send_error(403, "CGI script is not executable (%s)" %
132 `scriptname`)
133 return
134
135 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
136 # XXX Much of the following could be prepared ahead of time!
137 env = {}
138 env['SERVER_SOFTWARE'] = self.version_string()
139 env['SERVER_NAME'] = self.server.server_name
140 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
141 env['SERVER_PROTOCOL'] = self.protocol_version
142 env['SERVER_PORT'] = str(self.server.server_port)
143 env['REQUEST_METHOD'] = self.command
144 uqrest = urllib.unquote(rest)
145 env['PATH_INFO'] = uqrest
146 env['PATH_TRANSLATED'] = self.translate_path(uqrest)
147 env['SCRIPT_NAME'] = scriptname
148 if query:
149 env['QUERY_STRING'] = query
150 host = self.address_string()
151 if host != self.client_address[0]:
152 env['REMOTE_HOST'] = host
153 env['REMOTE_ADDR'] = self.client_address[0]
154 # XXX AUTH_TYPE
155 # XXX REMOTE_USER
156 # XXX REMOTE_IDENT
157 if self.headers.typeheader is None:
158 env['CONTENT_TYPE'] = self.headers.type
159 else:
160 env['CONTENT_TYPE'] = self.headers.typeheader
161 length = self.headers.getheader('content-length')
162 if length:
163 env['CONTENT_LENGTH'] = length
164 accept = []
165 for line in self.headers.getallmatchingheaders('accept'):
166 if line[:1] in string.whitespace:
167 accept.append(string.strip(line))
Guido van Rossume03c0501998-08-12 02:38:11 +0000168 else:
Jeremy Hyltond635b1d2000-09-26 17:32:27 +0000169 accept = accept + string.split(line[7:], ',')
170 env['HTTP_ACCEPT'] = string.joinfields(accept, ',')
171 ua = self.headers.getheader('user-agent')
172 if ua:
173 env['HTTP_USER_AGENT'] = ua
174 co = filter(None, self.headers.getheaders('cookie'))
175 if co:
176 env['HTTP_COOKIE'] = string.join(co, ', ')
177 # XXX Other HTTP_* headers
178 if not self.have_fork:
179 # Since we're setting the env in the parent, provide empty
180 # values to override previously set values
181 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
182 'HTTP_USER_AGENT', 'HTTP_COOKIE'):
183 env.setdefault(k, "")
184
185 self.send_response(200, "Script output follows")
186
187 decoded_query = string.replace(query, '+', ' ')
188
189 if self.have_fork:
190 # Unix -- fork as we should
191 args = [script]
192 if '=' not in decoded_query:
193 args.append(decoded_query)
194 nobody = nobody_uid()
195 self.wfile.flush() # Always flush before forking
196 pid = os.fork()
197 if pid != 0:
198 # Parent
199 pid, sts = os.waitpid(pid, 0)
200 if sts:
201 self.log_error("CGI script exit status %#x", sts)
202 return
203 # Child
Guido van Rossum548703a1998-03-26 22:14:20 +0000204 try:
Jeremy Hyltond635b1d2000-09-26 17:32:27 +0000205 try:
206 os.setuid(nobody)
207 except os.error:
208 pass
209 os.dup2(self.rfile.fileno(), 0)
210 os.dup2(self.wfile.fileno(), 1)
211 os.execve(scriptfile, args, env)
212 except:
213 self.server.handle_error(self.request, self.client_address)
214 os._exit(127)
215
216 elif self.have_popen2:
217 # Windows -- use popen2 to create a subprocess
218 import shutil
219 os.environ.update(env)
220 cmdline = scriptfile
221 if self.is_python(scriptfile):
222 interp = sys.executable
223 if interp.lower().endswith("w.exe"):
224 # On Windows, use python.exe, not python.exe
225 interp = interp[:-5] = interp[-4:]
226 cmdline = "%s %s" % (interp, cmdline)
227 if '=' not in query and '"' not in query:
228 cmdline = '%s "%s"' % (cmdline, query)
229 self.log_error("command: %s", cmdline)
230 try:
231 nbytes = int(length)
232 except:
233 nbytes = 0
234 fi, fo = os.popen2(cmdline)
235 if self.command.lower() == "post" and nbytes > 0:
236 data = self.rfile.read(nbytes)
237 fi.write(data)
238 fi.close()
239 shutil.copyfileobj(fo, self.wfile)
240 sts = fo.close()
241 if sts:
242 self.log_error("CGI script exit status %#x", sts)
243 else:
244 self.log_error("CGI script exited OK")
245
246 else:
247 # Other O.S. -- execute script in this process
248 os.environ.update(env)
249 save_argv = sys.argv
250 save_stdin = sys.stdin
251 save_stdout = sys.stdout
252 save_stderr = sys.stderr
253 try:
254 try:
255 sys.argv = [scriptfile]
256 if '=' not in decoded_query:
257 sys.argv.append(decoded_query)
258 sys.stdout = self.wfile
259 sys.stdin = self.rfile
260 execfile(scriptfile, {"__name__": "__main__"})
261 finally:
262 sys.argv = save_argv
263 sys.stdin = save_stdin
264 sys.stdout = save_stdout
265 sys.stderr = save_stderr
266 except SystemExit, sts:
267 self.log_error("CGI script exit status %s", str(sts))
268 else:
269 self.log_error("CGI script exited OK")
Guido van Rossum5c971671996-07-22 15:23:25 +0000270
271
272nobody = None
273
274def nobody_uid():
275 """Internal routine to get nobody's uid"""
276 global nobody
277 if nobody:
Guido van Rossum548703a1998-03-26 22:14:20 +0000278 return nobody
Jeremy Hyltond635b1d2000-09-26 17:32:27 +0000279 try:
280 import pwd
281 except ImportError:
282 return -1
Guido van Rossum5c971671996-07-22 15:23:25 +0000283 try:
Guido van Rossum548703a1998-03-26 22:14:20 +0000284 nobody = pwd.getpwnam('nobody')[2]
Guido van Rossumaad67612000-05-08 17:31:04 +0000285 except KeyError:
Guido van Rossum548703a1998-03-26 22:14:20 +0000286 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
Guido van Rossum5c971671996-07-22 15:23:25 +0000287 return nobody
288
289
290def executable(path):
291 """Test for executable file."""
292 try:
Guido van Rossum548703a1998-03-26 22:14:20 +0000293 st = os.stat(path)
Guido van Rossum5c971671996-07-22 15:23:25 +0000294 except os.error:
Guido van Rossum548703a1998-03-26 22:14:20 +0000295 return 0
Guido van Rossum5c971671996-07-22 15:23:25 +0000296 return st[0] & 0111 != 0
297
298
299def test(HandlerClass = CGIHTTPRequestHandler,
Guido van Rossum548703a1998-03-26 22:14:20 +0000300 ServerClass = BaseHTTPServer.HTTPServer):
Guido van Rossum5c971671996-07-22 15:23:25 +0000301 SimpleHTTPServer.test(HandlerClass, ServerClass)
302
303
304if __name__ == '__main__':
305 test()