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 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).
24 __all__
= ["CGIHTTPRequestHandler"]
30 import SimpleHTTPServer
34 class CGIHTTPRequestHandler(SimpleHTTPServer
.SimpleHTTPRequestHandler
):
36 """Complete HTTP server with GET, HEAD and POST commands.
38 GET and HEAD also support running CGI scripts.
40 The POST command is *only* implemented for CGI scripts.
44 # Determine platform specifics
45 have_fork
= hasattr(os
, 'fork')
46 have_popen2
= hasattr(os
, 'popen2')
47 have_popen3
= hasattr(os
, 'popen3')
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.
54 """Serve a POST request.
56 This is only implemented for CGI scripts.
63 self
.send_error(501, "Can only POST to CGI scripts")
66 """Version of send_head that support CGI scripts"""
70 return SimpleHTTPServer
.SimpleHTTPRequestHandler
.send_head(self
)
73 """Test whether self.path corresponds to a CGI script.
75 Returns True and updates the cgi_info attribute to the tuple
76 (dir, rest) if self.path requires running a CGI script.
77 Returns False otherwise.
79 The default implementation tests whether the normalized url
80 path begins with one of the strings in self.cgi_directories
81 (and the next character is a '/' or the end of the string).
83 splitpath
= _url_collapse_path_split(self
.path
)
84 if splitpath
[0] in self
.cgi_directories
:
85 self
.cgi_info
= splitpath
89 cgi_directories
= ['/cgi-bin', '/htbin']
91 def is_executable(self
, path
):
92 """Test whether argument path is an executable file."""
93 return executable(path
)
95 def is_python(self
, path
):
96 """Test whether argument path is a Python script."""
97 head
, tail
= os
.path
.splitext(path
)
98 return tail
.lower() in (".py", ".pyw")
101 """Execute a CGI script."""
103 dir, rest
= self
.cgi_info
105 i
= path
.find('/', len(dir) + 1)
108 nextrest
= path
[i
+1:]
110 scriptdir
= self
.translate_path(nextdir
)
111 if os
.path
.isdir(scriptdir
):
112 dir, rest
= nextdir
, nextrest
113 i
= path
.find('/', len(dir) + 1)
117 # find an explicit query string, if present.
120 rest
, query
= rest
[:i
], rest
[i
+1:]
124 # dissect the part after the directory name into a script name &
125 # a possible additional path, to be stored in PATH_INFO.
128 script
, rest
= rest
[:i
], rest
[i
:]
130 script
, rest
= rest
, ''
132 scriptname
= dir + '/' + script
133 scriptfile
= self
.translate_path(scriptname
)
134 if not os
.path
.exists(scriptfile
):
135 self
.send_error(404, "No such CGI script (%r)" % scriptname
)
137 if not os
.path
.isfile(scriptfile
):
138 self
.send_error(403, "CGI script is not a plain file (%r)" %
141 ispy
= self
.is_python(scriptname
)
143 if not (self
.have_fork
or self
.have_popen2
or self
.have_popen3
):
144 self
.send_error(403, "CGI script is not a Python script (%r)" %
147 if not self
.is_executable(scriptfile
):
148 self
.send_error(403, "CGI script is not executable (%r)" %
152 # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
153 # XXX Much of the following could be prepared ahead of time!
155 env
['SERVER_SOFTWARE'] = self
.version_string()
156 env
['SERVER_NAME'] = self
.server
.server_name
157 env
['GATEWAY_INTERFACE'] = 'CGI/1.1'
158 env
['SERVER_PROTOCOL'] = self
.protocol_version
159 env
['SERVER_PORT'] = str(self
.server
.server_port
)
160 env
['REQUEST_METHOD'] = self
.command
161 uqrest
= urllib
.unquote(rest
)
162 env
['PATH_INFO'] = uqrest
163 env
['PATH_TRANSLATED'] = self
.translate_path(uqrest
)
164 env
['SCRIPT_NAME'] = scriptname
166 env
['QUERY_STRING'] = query
167 host
= self
.address_string()
168 if host
!= self
.client_address
[0]:
169 env
['REMOTE_HOST'] = host
170 env
['REMOTE_ADDR'] = self
.client_address
[0]
171 authorization
= self
.headers
.getheader("authorization")
173 authorization
= authorization
.split()
174 if len(authorization
) == 2:
175 import base64
, binascii
176 env
['AUTH_TYPE'] = authorization
[0]
177 if authorization
[0].lower() == "basic":
179 authorization
= base64
.decodestring(authorization
[1])
180 except binascii
.Error
:
183 authorization
= authorization
.split(':')
184 if len(authorization
) == 2:
185 env
['REMOTE_USER'] = authorization
[0]
187 if self
.headers
.typeheader
is None:
188 env
['CONTENT_TYPE'] = self
.headers
.type
190 env
['CONTENT_TYPE'] = self
.headers
.typeheader
191 length
= self
.headers
.getheader('content-length')
193 env
['CONTENT_LENGTH'] = length
194 referer
= self
.headers
.getheader('referer')
196 env
['HTTP_REFERER'] = referer
198 for line
in self
.headers
.getallmatchingheaders('accept'):
199 if line
[:1] in "\t\n\r ":
200 accept
.append(line
.strip())
202 accept
= accept
+ line
[7:].split(',')
203 env
['HTTP_ACCEPT'] = ','.join(accept
)
204 ua
= self
.headers
.getheader('user-agent')
206 env
['HTTP_USER_AGENT'] = ua
207 co
= filter(None, self
.headers
.getheaders('cookie'))
209 env
['HTTP_COOKIE'] = ', '.join(co
)
210 # XXX Other HTTP_* headers
211 # Since we're setting the env in the parent, provide empty
212 # values to override previously set values
213 for k
in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
214 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
215 env
.setdefault(k
, "")
216 os
.environ
.update(env
)
218 self
.send_response(200, "Script output follows")
220 decoded_query
= query
.replace('+', ' ')
223 # Unix -- fork as we should
225 if '=' not in decoded_query
:
226 args
.append(decoded_query
)
227 nobody
= nobody_uid()
228 self
.wfile
.flush() # Always flush before forking
232 pid
, sts
= os
.waitpid(pid
, 0)
233 # throw away additional data [see bug #427345]
234 while select
.select([self
.rfile
], [], [], 0)[0]:
235 if not self
.rfile
.read(1):
238 self
.log_error("CGI script exit status %#x", sts
)
246 os
.dup2(self
.rfile
.fileno(), 0)
247 os
.dup2(self
.wfile
.fileno(), 1)
248 os
.execve(scriptfile
, args
, os
.environ
)
250 self
.server
.handle_error(self
.request
, self
.client_address
)
253 elif self
.have_popen2
or self
.have_popen3
:
254 # Windows -- use popen2 or popen3 to create a subprocess
261 if self
.is_python(scriptfile
):
262 interp
= sys
.executable
263 if interp
.lower().endswith("w.exe"):
264 # On Windows, use python.exe, not pythonw.exe
265 interp
= interp
[:-5] + interp
[-4:]
266 cmdline
= "%s -u %s" % (interp
, cmdline
)
267 if '=' not in query
and '"' not in query
:
268 cmdline
= '%s "%s"' % (cmdline
, query
)
269 self
.log_message("command: %s", cmdline
)
272 except (TypeError, ValueError):
274 files
= popenx(cmdline
, 'b')
279 if self
.command
.lower() == "post" and nbytes
> 0:
280 data
= self
.rfile
.read(nbytes
)
282 # throw away additional data [see bug #427345]
283 while select
.select([self
.rfile
._sock
], [], [], 0)[0]:
284 if not self
.rfile
._sock
.recv(1):
287 shutil
.copyfileobj(fo
, self
.wfile
)
292 self
.log_error('%s', errors
)
295 self
.log_error("CGI script exit status %#x", sts
)
297 self
.log_message("CGI script exited OK")
300 # Other O.S. -- execute script in this process
302 save_stdin
= sys
.stdin
303 save_stdout
= sys
.stdout
304 save_stderr
= sys
.stderr
306 save_cwd
= os
.getcwd()
308 sys
.argv
= [scriptfile
]
309 if '=' not in decoded_query
:
310 sys
.argv
.append(decoded_query
)
311 sys
.stdout
= self
.wfile
312 sys
.stdin
= self
.rfile
313 execfile(scriptfile
, {"__name__": "__main__"})
316 sys
.stdin
= save_stdin
317 sys
.stdout
= save_stdout
318 sys
.stderr
= save_stderr
320 except SystemExit, sts
:
321 self
.log_error("CGI script exit status %s", str(sts
))
323 self
.log_message("CGI script exited OK")
326 # TODO(gregory.p.smith): Move this into an appropriate library.
327 def _url_collapse_path_split(path
):
329 Given a URL path, remove extra '/'s and '.' path elements and collapse
332 Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
334 Returns: A tuple of (head, tail) where tail is everything after the final /
335 and head is everything before it. Head will always start with a '/' and,
336 if it contains anything else, never have a trailing '/'.
338 Raises: IndexError if too many '..' occur within the path.
340 # Similar to os.path.split(os.path.normpath(path)) but specific to URL
341 # path semantics rather than local operating system semantics.
343 for part
in path
.split('/'):
345 path_parts
.append('')
347 path_parts
.append(part
)
348 # Filter out blank non trailing parts before consuming the '..'.
349 path_parts
= [part
for part
in path_parts
[:-1] if part
] + path_parts
[-1:]
351 tail_part
= path_parts
.pop()
355 for part
in path_parts
:
359 head_parts
.append(part
)
360 if tail_part
and tail_part
== '..':
363 return ('/' + '/'.join(head_parts
), tail_part
)
369 """Internal routine to get nobody's uid"""
378 nobody
= pwd
.getpwnam('nobody')[2]
380 nobody
= 1 + max(map(lambda x
: x
[2], pwd
.getpwall()))
384 def executable(path
):
385 """Test for executable file."""
390 return st
.st_mode
& 0111 != 0
393 def test(HandlerClass
= CGIHTTPRequestHandler
,
394 ServerClass
= BaseHTTPServer
.HTTPServer
):
395 SimpleHTTPServer
.test(HandlerClass
, ServerClass
)
398 if __name__
== '__main__':