source: python/trunk/Lib/CGIHTTPServer.py@ 348

Last change on this file since 348 was 2, checked in by Yuri Dario, 15 years ago

Initial import for vendor code.

  • Property svn:eol-style set to native
File size: 11.2 KB
Line 
1"""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 and return a boolean.
75
76 This function sets self.cgi_info to a tuple (dir, rest)
77 when it returns True, where dir is the directory part before
78 the CGI script name. Note that rest begins with a
79 slash if it is not empty.
80
81 The default implementation tests whether the path
82 begins with one of the strings in the list
83 self.cgi_directories (and the next character is a '/'
84 or the end of the string).
85 """
86
87 path = self.path
88
89 for x in self.cgi_directories:
90 i = len(x)
91 if path[:i] == x and (not path[i:] or path[i] == '/'):
92 self.cgi_info = path[:i], path[i+1:]
93 return True
94 return False
95
96 cgi_directories = ['/cgi-bin', '/htbin']
97
98 def is_executable(self, path):
99 """Test whether argument path is an executable file."""
100 return executable(path)
101
102 def is_python(self, path):
103 """Test whether argument path is a Python script."""
104 head, tail = os.path.splitext(path)
105 return tail.lower() in (".py", ".pyw")
106
107 def run_cgi(self):
108 """Execute a CGI script."""
109 path = self.path
110 dir, rest = self.cgi_info
111
112 i = path.find('/', len(dir) + 1)
113 while i >= 0:
114 nextdir = path[:i]
115 nextrest = path[i+1:]
116
117 scriptdir = self.translate_path(nextdir)
118 if os.path.isdir(scriptdir):
119 dir, rest = nextdir, nextrest
120 i = path.find('/', len(dir) + 1)
121 else:
122 break
123
124 # find an explicit query string, if present.
125 i = rest.rfind('?')
126 if i >= 0:
127 rest, query = rest[:i], rest[i+1:]
128 else:
129 query = ''
130
131 # dissect the part after the directory name into a script name &
132 # a possible additional path, to be stored in PATH_INFO.
133 i = rest.find('/')
134 if i >= 0:
135 script, rest = rest[:i], rest[i:]
136 else:
137 script, rest = rest, ''
138
139 scriptname = dir + '/' + script
140 scriptfile = self.translate_path(scriptname)
141 if not os.path.exists(scriptfile):
142 self.send_error(404, "No such CGI script (%r)" % scriptname)
143 return
144 if not os.path.isfile(scriptfile):
145 self.send_error(403, "CGI script is not a plain file (%r)" %
146 scriptname)
147 return
148 ispy = self.is_python(scriptname)
149 if not ispy:
150 if not (self.have_fork or self.have_popen2 or self.have_popen3):
151 self.send_error(403, "CGI script is not a Python script (%r)" %
152 scriptname)
153 return
154 if not self.is_executable(scriptfile):
155 self.send_error(403, "CGI script is not executable (%r)" %
156 scriptname)
157 return
158
159 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
160 # XXX Much of the following could be prepared ahead of time!
161 env = {}
162 env['SERVER_SOFTWARE'] = self.version_string()
163 env['SERVER_NAME'] = self.server.server_name
164 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
165 env['SERVER_PROTOCOL'] = self.protocol_version
166 env['SERVER_PORT'] = str(self.server.server_port)
167 env['REQUEST_METHOD'] = self.command
168 uqrest = urllib.unquote(rest)
169 env['PATH_INFO'] = uqrest
170 env['PATH_TRANSLATED'] = self.translate_path(uqrest)
171 env['SCRIPT_NAME'] = scriptname
172 if query:
173 env['QUERY_STRING'] = query
174 host = self.address_string()
175 if host != self.client_address[0]:
176 env['REMOTE_HOST'] = host
177 env['REMOTE_ADDR'] = self.client_address[0]
178 authorization = self.headers.getheader("authorization")
179 if authorization:
180 authorization = authorization.split()
181 if len(authorization) == 2:
182 import base64, binascii
183 env['AUTH_TYPE'] = authorization[0]
184 if authorization[0].lower() == "basic":
185 try:
186 authorization = base64.decodestring(authorization[1])
187 except binascii.Error:
188 pass
189 else:
190 authorization = authorization.split(':')
191 if len(authorization) == 2:
192 env['REMOTE_USER'] = authorization[0]
193 # XXX REMOTE_IDENT
194 if self.headers.typeheader is None:
195 env['CONTENT_TYPE'] = self.headers.type
196 else:
197 env['CONTENT_TYPE'] = self.headers.typeheader
198 length = self.headers.getheader('content-length')
199 if length:
200 env['CONTENT_LENGTH'] = length
201 referer = self.headers.getheader('referer')
202 if referer:
203 env['HTTP_REFERER'] = referer
204 accept = []
205 for line in self.headers.getallmatchingheaders('accept'):
206 if line[:1] in "\t\n\r ":
207 accept.append(line.strip())
208 else:
209 accept = accept + line[7:].split(',')
210 env['HTTP_ACCEPT'] = ','.join(accept)
211 ua = self.headers.getheader('user-agent')
212 if ua:
213 env['HTTP_USER_AGENT'] = ua
214 co = filter(None, self.headers.getheaders('cookie'))
215 if co:
216 env['HTTP_COOKIE'] = ', '.join(co)
217 # XXX Other HTTP_* headers
218 # Since we're setting the env in the parent, provide empty
219 # values to override previously set values
220 for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
221 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
222 env.setdefault(k, "")
223 os.environ.update(env)
224
225 self.send_response(200, "Script output follows")
226
227 decoded_query = query.replace('+', ' ')
228
229 if self.have_fork:
230 # Unix -- fork as we should
231 args = [script]
232 if '=' not in decoded_query:
233 args.append(decoded_query)
234 nobody = nobody_uid()
235 self.wfile.flush() # Always flush before forking
236 pid = os.fork()
237 if pid != 0:
238 # Parent
239 pid, sts = os.waitpid(pid, 0)
240 # throw away additional data [see bug #427345]
241 while select.select([self.rfile], [], [], 0)[0]:
242 if not self.rfile.read(1):
243 break
244 if sts:
245 self.log_error("CGI script exit status %#x", sts)
246 return
247 # Child
248 try:
249 try:
250 os.setuid(nobody)
251 except os.error:
252 pass
253 os.dup2(self.rfile.fileno(), 0)
254 os.dup2(self.wfile.fileno(), 1)
255 os.execve(scriptfile, args, os.environ)
256 except:
257 self.server.handle_error(self.request, self.client_address)
258 os._exit(127)
259
260 else:
261 # Non Unix - use subprocess
262 import subprocess
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 = [interp, '-u'] + cmdline
270 if '=' not in query:
271 cmdline.append(query)
272
273 self.log_message("command: %s", subprocess.list2cmdline(cmdline))
274 try:
275 nbytes = int(length)
276 except (TypeError, ValueError):
277 nbytes = 0
278 p = subprocess.Popen(cmdline,
279 stdin = subprocess.PIPE,
280 stdout = subprocess.PIPE,
281 stderr = subprocess.PIPE
282 )
283 if self.command.lower() == "post" and nbytes > 0:
284 data = self.rfile.read(nbytes)
285 else:
286 data = None
287 # throw away additional data [see bug #427345]
288 while select.select([self.rfile._sock], [], [], 0)[0]:
289 if not self.rfile._sock.recv(1):
290 break
291 stdout, stderr = p.communicate(data)
292 self.wfile.write(stdout)
293 if stderr:
294 self.log_error('%s', stderr)
295 status = p.returncode
296 if status:
297 self.log_error("CGI script exit status %#x", status)
298 else:
299 self.log_message("CGI script exited OK")
300
301
302nobody = None
303
304def nobody_uid():
305 """Internal routine to get nobody's uid"""
306 global nobody
307 if nobody:
308 return nobody
309 try:
310 import pwd
311 except ImportError:
312 return -1
313 try:
314 nobody = pwd.getpwnam('nobody')[2]
315 except KeyError:
316 nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
317 return nobody
318
319
320def executable(path):
321 """Test for executable file."""
322 try:
323 st = os.stat(path)
324 except os.error:
325 return False
326 return st.st_mode & 0111 != 0
327
328
329def test(HandlerClass = CGIHTTPRequestHandler,
330 ServerClass = BaseHTTPServer.HTTPServer):
331 SimpleHTTPServer.test(HandlerClass, ServerClass)
332
333
334if __name__ == '__main__':
335 test()
Note: See TracBrowser for help on using the repository browser.