diff options
-rw-r--r-- | Lib/CGIHTTPServer.py | 254 |
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: |