cgihttps.py 6.31 KB
Newer Older
1 2 3 4 5
"""CGI-savvy HTTP Server.

This module builds on SimpleHTTPServer by implementing GET and POST
requests to cgi-bin scripts.

Guido van Rossum's avatar
Guido van Rossum committed
6 7 8
If the os.fork() function is not present, this module will not work;
SystemError will be raised instead.

9 10 11 12 13 14 15 16 17 18 19 20 21
"""


__version__ = "0.3"


import os
import string
import urllib
import BaseHTTPServer
import SimpleHTTPServer


Guido van Rossum's avatar
Guido van Rossum committed
22 23 24 25 26 27
try:
    os.fork
except AttributeError:
    raise SystemError, __name__ + " requires os.fork()"


28 29 30 31 32 33 34 35 36 37 38
class CGIHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):

    """Complete HTTP server with GET, HEAD and POST commands.

    GET and HEAD also support running CGI scripts.

    The POST command is *only* implemented for CGI scripts.

    """

    def do_POST(self):
Guido van Rossum's avatar
Guido van Rossum committed
39
        """Serve a POST request.
40

Guido van Rossum's avatar
Guido van Rossum committed
41
        This is only implemented for CGI scripts.
42

Guido van Rossum's avatar
Guido van Rossum committed
43
        """
44

Guido van Rossum's avatar
Guido van Rossum committed
45 46 47 48
        if self.is_cgi():
            self.run_cgi()
        else:
            self.send_error(501, "Can only POST to CGI scripts")
49 50

    def send_head(self):
Guido van Rossum's avatar
Guido van Rossum committed
51 52 53 54 55
        """Version of send_head that support CGI scripts"""
        if self.is_cgi():
            return self.run_cgi()
        else:
            return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
56 57

    def is_cgi(self):
Guido van Rossum's avatar
Guido van Rossum committed
58
        """test whether PATH corresponds to a CGI script.
59

Guido van Rossum's avatar
Guido van Rossum committed
60 61 62
        Return a tuple (dir, rest) if PATH requires running a
        CGI script, None if not.  Note that rest begins with a
        slash if it is not empty.
63

Guido van Rossum's avatar
Guido van Rossum committed
64 65 66 67
        The default implementation tests whether the path
        begins with one of the strings in the list
        self.cgi_directories (and the next character is a '/'
        or the end of the string).
68

Guido van Rossum's avatar
Guido van Rossum committed
69
        """
70

Guido van Rossum's avatar
Guido van Rossum committed
71
        path = self.path
72

Guido van Rossum's avatar
Guido van Rossum committed
73 74 75 76 77 78
        for x in self.cgi_directories:
            i = len(x)
            if path[:i] == x and (not path[i:] or path[i] == '/'):
                self.cgi_info = path[:i], path[i+1:]
                return 1
        return 0
79 80 81 82

    cgi_directories = ['/cgi-bin', '/htbin']

    def run_cgi(self):
Guido van Rossum's avatar
Guido van Rossum committed
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
        """Execute a CGI script."""
        dir, rest = self.cgi_info
        i = string.rfind(rest, '?')
        if i >= 0:
            rest, query = rest[:i], rest[i+1:]
        else:
            query = ''
        i = string.find(rest, '/')
        if i >= 0:
            script, rest = rest[:i], rest[i:]
        else:
            script, rest = rest, ''
        scriptname = dir + '/' + script
        scriptfile = self.translate_path(scriptname)
        if not os.path.exists(scriptfile):
            self.send_error(404, "No such CGI script (%s)" % `scriptname`)
            return
        if not os.path.isfile(scriptfile):
            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()
        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)
            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
Guido van Rossum's avatar
Guido van Rossum committed
142 143 144 145
            if self.headers.typeheader is None:
                env['CONTENT_TYPE'] = self.headers.type
            else:
                env['CONTENT_TYPE'] = self.headers.typeheader
Guido van Rossum's avatar
Guido van Rossum committed
146 147 148 149 150 151 152 153
            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:
Guido van Rossum's avatar
Guido van Rossum committed
154
                    accept = accept + string.split(line[7:], ',')
Guido van Rossum's avatar
Guido van Rossum committed
155 156 157 158
            env['HTTP_ACCEPT'] = string.joinfields(accept, ',')
            ua = self.headers.getheader('user-agent')
            if ua:
                env['HTTP_USER_AGENT'] = ua
Guido van Rossum's avatar
Guido van Rossum committed
159 160 161
            co = filter(None, self.headers.getheaders('cookie'))
            if co:
                env['HTTP_COOKIE'] = string.join(co, ', ')
Guido van Rossum's avatar
Guido van Rossum committed
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
            # XXX Other HTTP_* headers
            decoded_query = string.replace(query, '+', ' ')
            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)
177 178 179 180 181 182 183 184


nobody = None

def nobody_uid():
    """Internal routine to get nobody's uid"""
    global nobody
    if nobody:
Guido van Rossum's avatar
Guido van Rossum committed
185
        return nobody
186 187
    import pwd
    try:
Guido van Rossum's avatar
Guido van Rossum committed
188
        nobody = pwd.getpwnam('nobody')[2]
Guido van Rossum's avatar
Guido van Rossum committed
189
    except KeyError:
Guido van Rossum's avatar
Guido van Rossum committed
190
        nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
191 192 193 194 195 196
    return nobody


def executable(path):
    """Test for executable file."""
    try:
Guido van Rossum's avatar
Guido van Rossum committed
197
        st = os.stat(path)
198
    except os.error:
Guido van Rossum's avatar
Guido van Rossum committed
199
        return 0
200 201 202 203
    return st[0] & 0111 != 0


def test(HandlerClass = CGIHTTPRequestHandler,
Guido van Rossum's avatar
Guido van Rossum committed
204
         ServerClass = BaseHTTPServer.HTTPServer):
205 206 207 208 209
    SimpleHTTPServer.test(HandlerClass, ServerClass)


if __name__ == '__main__':
    test()