PageRenderTime 33ms CodeModel.GetById 2ms app.highlight 27ms RepoModel.GetById 1ms app.codeStats 0ms

/Lib/CGIHTTPServer.py

http://unladen-swallow.googlecode.com/
Python | 366 lines | 339 code | 6 blank | 21 comment | 26 complexity | df6728d2511e83f2fdc2006a2b18c067 MD5 | raw file
  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        elif self.have_popen2 or self.have_popen3:
261            # Windows -- use popen2 or popen3 to create a subprocess
262            import shutil
263            if self.have_popen3:
264                popenx = os.popen3
265            else:
266                popenx = os.popen2
267            cmdline = scriptfile
268            if self.is_python(scriptfile):
269                interp = sys.executable
270                if interp.lower().endswith("w.exe"):
271                    # On Windows, use python.exe, not pythonw.exe
272                    interp = interp[:-5] + interp[-4:]
273                cmdline = "%s -u %s" % (interp, cmdline)
274            if '=' not in query and '"' not in query:
275                cmdline = '%s "%s"' % (cmdline, query)
276            self.log_message("command: %s", cmdline)
277            try:
278                nbytes = int(length)
279            except (TypeError, ValueError):
280                nbytes = 0
281            files = popenx(cmdline, 'b')
282            fi = files[0]
283            fo = files[1]
284            if self.have_popen3:
285                fe = files[2]
286            if self.command.lower() == "post" and nbytes > 0:
287                data = self.rfile.read(nbytes)
288                fi.write(data)
289            # throw away additional data [see bug #427345]
290            while select.select([self.rfile._sock], [], [], 0)[0]:
291                if not self.rfile._sock.recv(1):
292                    break
293            fi.close()
294            shutil.copyfileobj(fo, self.wfile)
295            if self.have_popen3:
296                errors = fe.read()
297                fe.close()
298                if errors:
299                    self.log_error('%s', errors)
300            sts = fo.close()
301            if sts:
302                self.log_error("CGI script exit status %#x", sts)
303            else:
304                self.log_message("CGI script exited OK")
305
306        else:
307            # Other O.S. -- execute script in this process
308            save_argv = sys.argv
309            save_stdin = sys.stdin
310            save_stdout = sys.stdout
311            save_stderr = sys.stderr
312            try:
313                save_cwd = os.getcwd()
314                try:
315                    sys.argv = [scriptfile]
316                    if '=' not in decoded_query:
317                        sys.argv.append(decoded_query)
318                    sys.stdout = self.wfile
319                    sys.stdin = self.rfile
320                    execfile(scriptfile, {"__name__": "__main__"})
321                finally:
322                    sys.argv = save_argv
323                    sys.stdin = save_stdin
324                    sys.stdout = save_stdout
325                    sys.stderr = save_stderr
326                    os.chdir(save_cwd)
327            except SystemExit, sts:
328                self.log_error("CGI script exit status %s", str(sts))
329            else:
330                self.log_message("CGI script exited OK")
331
332
333nobody = None
334
335def nobody_uid():
336    """Internal routine to get nobody's uid"""
337    global nobody
338    if nobody:
339        return nobody
340    try:
341        import pwd
342    except ImportError:
343        return -1
344    try:
345        nobody = pwd.getpwnam('nobody')[2]
346    except KeyError:
347        nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
348    return nobody
349
350
351def executable(path):
352    """Test for executable file."""
353    try:
354        st = os.stat(path)
355    except os.error:
356        return False
357    return st.st_mode & 0111 != 0
358
359
360def test(HandlerClass = CGIHTTPRequestHandler,
361         ServerClass = BaseHTTPServer.HTTPServer):
362    SimpleHTTPServer.test(HandlerClass, ServerClass)
363
364
365if __name__ == '__main__':
366    test()