summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/CGIHTTPServer.py254
1 files changed, 173 insertions, 81 deletions
diff --git a/Lib/CGIHTTPServer.py b/Lib/CGIHTTPServer.py
index 6a259a3..ba1e76b 100644
--- a/Lib/CGIHTTPServer.py
+++ b/Lib/CGIHTTPServer.py
@@ -3,28 +3,31 @@
This module builds on SimpleHTTPServer by implementing GET and POST
requests to cgi-bin scripts.
-If the os.fork() function is not present, this module will not work;
-SystemError will be raised instead.
+If the os.fork() function is not present (e.g. on Windows),
+os.popen2() is used as a fallback, with slightly altered semantics; if
+that function is not present either (e.g. on Macintosh), only Python
+scripts are supported, and they are executed by the current process.
+
+In all cases, the implementation is intentionally naive -- all
+requests are executed sychronously.
+
+SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
+-- it may execute arbitrary Python code or external programs.
"""
-__version__ = "0.3"
+__version__ = "0.4"
import os
+import sys
import string
import urllib
import BaseHTTPServer
import SimpleHTTPServer
-try:
- os.fork
-except AttributeError:
- raise SystemError, __name__ + " requires os.fork()"
-
-
class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
"""Complete HTTP server with GET, HEAD and POST commands.
@@ -35,6 +38,10 @@ class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
"""
+ # Determine platform specifics
+ have_fork = hasattr(os, 'fork')
+ have_popen2 = hasattr(os, 'popen2')
+
# Make rfile unbuffered -- we need to read one line and then pass
# the rest to a subprocess, so we can't use buffered input.
rbufsize = 0
@@ -59,9 +66,9 @@ class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
def is_cgi(self):
- """test whether PATH corresponds to a CGI script.
+ """Test whether self.path corresponds to a CGI script.
- Return a tuple (dir, rest) if PATH requires running a
+ Return a tuple (dir, rest) if self.path requires running a
CGI script, None if not. Note that rest begins with a
slash if it is not empty.
@@ -83,6 +90,15 @@ class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
cgi_directories = ['/cgi-bin', '/htbin']
+ def is_executable(self, path):
+ """Test whether argument path is an executable file."""
+ return executable(path)
+
+ def is_python(self, path):
+ """Test whether argument path is a Python script."""
+ head, tail = os.path.splitext(path)
+ return tail.lower() in (".py", ".pyw")
+
def run_cgi(self):
"""Execute a CGI script."""
dir, rest = self.cgi_info
@@ -105,79 +121,152 @@ class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
self.send_error(403, "CGI script is not a plain file (%s)" %
`scriptname`)
return
- if not executable(scriptfile):
- self.send_error(403, "CGI script is not executable (%s)" %
- `scriptname`)
- return
- nobody = nobody_uid()
+ ispy = self.is_python(scriptname)
+ if not ispy:
+ if not (self.have_fork or self.have_popen2):
+ self.send_error(403, "CGI script is not a Python script (%s)" %
+ `scriptname`)
+ return
+ if not self.is_executable(scriptfile):
+ self.send_error(403, "CGI script is not executable (%s)" %
+ `scriptname`)
+ return
+
+ # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
+ # XXX Much of the following could be prepared ahead of time!
+ env = {}
+ env['SERVER_SOFTWARE'] = self.version_string()
+ env['SERVER_NAME'] = self.server.server_name
+ env['GATEWAY_INTERFACE'] = 'CGI/1.1'
+ env['SERVER_PROTOCOL'] = self.protocol_version
+ env['SERVER_PORT'] = str(self.server.server_port)
+ env['REQUEST_METHOD'] = self.command
+ uqrest = urllib.unquote(rest)
+ env['PATH_INFO'] = uqrest
+ env['PATH_TRANSLATED'] = self.translate_path(uqrest)
+ env['SCRIPT_NAME'] = scriptname
+ if query:
+ env['QUERY_STRING'] = query
+ host = self.address_string()
+ if host != self.client_address[0]:
+ env['REMOTE_HOST'] = host
+ env['REMOTE_ADDR'] = self.client_address[0]
+ # XXX AUTH_TYPE
+ # XXX REMOTE_USER
+ # XXX REMOTE_IDENT
+ if self.headers.typeheader is None:
+ env['CONTENT_TYPE'] = self.headers.type
+ else:
+ env['CONTENT_TYPE'] = self.headers.typeheader
+ length = self.headers.getheader('content-length')
+ if length:
+ env['CONTENT_LENGTH'] = length
+ accept = []
+ for line in self.headers.getallmatchingheaders('accept'):
+ if line[:1] in string.whitespace:
+ accept.append(string.strip(line))
+ else:
+ accept = accept + string.split(line[7:], ',')
+ env['HTTP_ACCEPT'] = string.joinfields(accept, ',')
+ ua = self.headers.getheader('user-agent')
+ if ua:
+ env['HTTP_USER_AGENT'] = ua
+ co = filter(None, self.headers.getheaders('cookie'))
+ if co:
+ env['HTTP_COOKIE'] = string.join(co, ', ')
+ # XXX Other HTTP_* headers
+ if not self.have_fork:
+ # Since we're setting the env in the parent, provide empty
+ # values to override previously set values
+ for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
+ 'HTTP_USER_AGENT', 'HTTP_COOKIE'):
+ env.setdefault(k, "")
+
self.send_response(200, "Script output follows")
- self.wfile.flush() # Always flush before forking
- pid = os.fork()
- if pid != 0:
- # Parent
- pid, sts = os.waitpid(pid, 0)
+
+ decoded_query = string.replace(query, '+', ' ')
+
+ if self.have_fork:
+ # Unix -- fork as we should
+ args = [script]
+ if '=' not in decoded_query:
+ args.append(decoded_query)
+ nobody = nobody_uid()
+ self.wfile.flush() # Always flush before forking
+ pid = os.fork()
+ if pid != 0:
+ # Parent
+ pid, sts = os.waitpid(pid, 0)
+ if sts:
+ self.log_error("CGI script exit status %#x", sts)
+ return
+ # Child
+ try:
+ try:
+ os.setuid(nobody)
+ except os.error:
+ pass
+ os.dup2(self.rfile.fileno(), 0)
+ os.dup2(self.wfile.fileno(), 1)
+ os.execve(scriptfile, args, env)
+ except:
+ self.server.handle_error(self.request, self.client_address)
+ os._exit(127)
+
+ elif self.have_popen2:
+ # Windows -- use popen2 to create a subprocess
+ import shutil
+ os.environ.update(env)
+ cmdline = scriptfile
+ if self.is_python(scriptfile):
+ interp = sys.executable
+ if interp.lower().endswith("w.exe"):
+ # On Windows, use python.exe, not python.exe
+ interp = interp[:-5] = interp[-4:]
+ cmdline = "%s %s" % (interp, cmdline)
+ if '=' not in query and '"' not in query:
+ cmdline = '%s "%s"' % (cmdline, query)
+ self.log_error("command: %s", cmdline)
+ try:
+ nbytes = int(length)
+ except:
+ nbytes = 0
+ fi, fo = os.popen2(cmdline)
+ if self.command.lower() == "post" and nbytes > 0:
+ data = self.rfile.read(nbytes)
+ fi.write(data)
+ fi.close()
+ shutil.copyfileobj(fo, self.wfile)
+ sts = fo.close()
if sts:
- self.log_error("CGI script exit status x%x" % sts)
- return
- # Child
- try:
- # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
- # XXX Much of the following could be prepared ahead of time!
- env = {}
- env['SERVER_SOFTWARE'] = self.version_string()
- env['SERVER_NAME'] = self.server.server_name
- env['GATEWAY_INTERFACE'] = 'CGI/1.1'
- env['SERVER_PROTOCOL'] = self.protocol_version
- env['SERVER_PORT'] = str(self.server.server_port)
- env['REQUEST_METHOD'] = self.command
- uqrest = urllib.unquote(rest)
- env['PATH_INFO'] = uqrest
- env['PATH_TRANSLATED'] = self.translate_path(uqrest)
- env['SCRIPT_NAME'] = scriptname
- if query:
- env['QUERY_STRING'] = query
- host = self.address_string()
- if host != self.client_address[0]:
- env['REMOTE_HOST'] = host
- env['REMOTE_ADDR'] = self.client_address[0]
- # AUTH_TYPE
- # REMOTE_USER
- # REMOTE_IDENT
- if self.headers.typeheader is None:
- env['CONTENT_TYPE'] = self.headers.type
+ self.log_error("CGI script exit status %#x", sts)
else:
- env['CONTENT_TYPE'] = self.headers.typeheader
- length = self.headers.getheader('content-length')
- if length:
- env['CONTENT_LENGTH'] = length
- accept = []
- for line in self.headers.getallmatchingheaders('accept'):
- if line[:1] in string.whitespace:
- accept.append(string.strip(line))
- else:
- accept = accept + string.split(line[7:], ',')
- env['HTTP_ACCEPT'] = string.joinfields(accept, ',')
- ua = self.headers.getheader('user-agent')
- if ua:
- env['HTTP_USER_AGENT'] = ua
- co = filter(None, self.headers.getheaders('cookie'))
- if co:
- env['HTTP_COOKIE'] = string.join(co, ', ')
- # XXX Other HTTP_* headers
- decoded_query = string.replace(query, '+', ' ')
+ self.log_error("CGI script exited OK")
+
+ else:
+ # Other O.S. -- execute script in this process
+ os.environ.update(env)
+ save_argv = sys.argv
+ save_stdin = sys.stdin
+ save_stdout = sys.stdout
+ save_stderr = sys.stderr
try:
- os.setuid(nobody)
- except os.error:
- pass
- os.dup2(self.rfile.fileno(), 0)
- os.dup2(self.wfile.fileno(), 1)
- print scriptfile, script, decoded_query
- os.execve(scriptfile,
- [script, decoded_query],
- env)
- except:
- self.server.handle_error(self.request, self.client_address)
- os._exit(127)
+ try:
+ sys.argv = [scriptfile]
+ if '=' not in decoded_query:
+ sys.argv.append(decoded_query)
+ sys.stdout = self.wfile
+ sys.stdin = self.rfile
+ execfile(scriptfile, {"__name__": "__main__"})
+ finally:
+ sys.argv = save_argv
+ sys.stdin = save_stdin
+ sys.stdout = save_stdout
+ sys.stderr = save_stderr
+ except SystemExit, sts:
+ self.log_error("CGI script exit status %s", str(sts))
+ else:
+ self.log_error("CGI script exited OK")
nobody = None
@@ -187,7 +276,10 @@ def nobody_uid():
global nobody
if nobody:
return nobody
- import pwd
+ try:
+ import pwd
+ except ImportError:
+ return -1
try:
nobody = pwd.getpwnam('nobody')[2]
except KeyError: