CGIHTTPServer.py 11.6 KB
Newer Older
Guido van Rossum's avatar
Guido van Rossum committed
1 2 3 4 5
"""CGI-savvy HTTP Server.

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

6 7 8 9 10 11 12 13 14 15
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.
16

17 18
Note that status code 200 is sent prior to execution of a CGI script, so
scripts cannot send other status codes such as 302 (redirect).
Guido van Rossum's avatar
Guido van Rossum committed
19 20 21
"""


22
__version__ = "0.4"
Guido van Rossum's avatar
Guido van Rossum committed
23

24
__all__ = ["CGIHTTPRequestHandler"]
Guido van Rossum's avatar
Guido van Rossum committed
25 26

import os
27
import sys
Guido van Rossum's avatar
Guido van Rossum committed
28 29 30
import urllib
import BaseHTTPServer
import SimpleHTTPServer
31
import select
Guido van Rossum's avatar
Guido van Rossum committed
32 33 34 35 36 37 38 39 40 41 42 43


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.

    """

44 45 46
    # Determine platform specifics
    have_fork = hasattr(os, 'fork')
    have_popen2 = hasattr(os, 'popen2')
47
    have_popen3 = hasattr(os, 'popen3')
48

49 50 51 52
    # 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

Guido van Rossum's avatar
Guido van Rossum committed
53
    def do_POST(self):
54
        """Serve a POST request.
Guido van Rossum's avatar
Guido van Rossum committed
55

56
        This is only implemented for CGI scripts.
Guido van Rossum's avatar
Guido van Rossum committed
57

58
        """
Guido van Rossum's avatar
Guido van Rossum committed
59

60 61 62 63
        if self.is_cgi():
            self.run_cgi()
        else:
            self.send_error(501, "Can only POST to CGI scripts")
Guido van Rossum's avatar
Guido van Rossum committed
64 65

    def send_head(self):
66 67 68 69 70
        """Version of send_head that support CGI scripts"""
        if self.is_cgi():
            return self.run_cgi()
        else:
            return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
Guido van Rossum's avatar
Guido van Rossum committed
71 72

    def is_cgi(self):
73
        """Test whether self.path corresponds to a CGI script.
Guido van Rossum's avatar
Guido van Rossum committed
74

75
        Return a tuple (dir, rest) if self.path requires running a
76 77
        CGI script, None if not.  Note that rest begins with a
        slash if it is not empty.
Guido van Rossum's avatar
Guido van Rossum committed
78

79 80 81 82
        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).
Guido van Rossum's avatar
Guido van Rossum committed
83

84
        """
Guido van Rossum's avatar
Guido van Rossum committed
85

86
        path = self.path
Guido van Rossum's avatar
Guido van Rossum committed
87

88 89 90 91
        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:]
92 93
                return True
        return False
Guido van Rossum's avatar
Guido van Rossum committed
94 95 96

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

97 98 99 100 101 102 103 104 105
    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")

Guido van Rossum's avatar
Guido van Rossum committed
106
    def run_cgi(self):
107 108
        """Execute a CGI script."""
        dir, rest = self.cgi_info
109
        i = rest.rfind('?')
110 111 112 113
        if i >= 0:
            rest, query = rest[:i], rest[i+1:]
        else:
            query = ''
114
        i = rest.find('/')
115 116 117 118 119 120 121
        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):
122
            self.send_error(404, "No such CGI script (%r)" % scriptname)
123 124
            return
        if not os.path.isfile(scriptfile):
125
            self.send_error(403, "CGI script is not a plain file (%r)" %
126
                            scriptname)
127
            return
128 129
        ispy = self.is_python(scriptname)
        if not ispy:
130
            if not (self.have_fork or self.have_popen2 or self.have_popen3):
131 132
                self.send_error(403, "CGI script is not a Python script (%r)" %
                                scriptname)
133 134
                return
            if not self.is_executable(scriptfile):
135 136
                self.send_error(403, "CGI script is not executable (%r)" %
                                scriptname)
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
                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]
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
        authorization = self.headers.getheader("authorization")
        if authorization:
            authorization = authorization.split()
            if len(authorization) == 2:
                import base64, binascii
                env['AUTH_TYPE'] = authorization[0]
                if authorization[0].lower() == "basic":
                    try:
                        authorization = base64.decodestring(authorization[1])
                    except binascii.Error:
                        pass
                    else:
                        authorization = authorization.split(':')
                        if len(authorization) == 2:
                            env['REMOTE_USER'] = authorization[0]
173 174 175 176 177 178 179 180 181 182
        # 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'):
183
            if line[:1] in "\t\n\r ":
184
                accept.append(line.strip())
185
            else:
186 187
                accept = accept + line[7:].split(',')
        env['HTTP_ACCEPT'] = ','.join(accept)
188 189 190 191 192
        ua = self.headers.getheader('user-agent')
        if ua:
            env['HTTP_USER_AGENT'] = ua
        co = filter(None, self.headers.getheaders('cookie'))
        if co:
193
            env['HTTP_COOKIE'] = ', '.join(co)
194
        # XXX Other HTTP_* headers
Guido van Rossum's avatar
Guido van Rossum committed
195 196 197 198 199
        # 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, "")
200
        os.environ.update(env)
201

202
        self.send_response(200, "Script output follows")
203

204
        decoded_query = query.replace('+', ' ')
205 206 207 208 209 210 211 212 213 214 215 216

        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)
217 218
                # throw away additional data [see bug #427345]
                while select.select([self.rfile], [], [], 0)[0]:
219 220
                    if not self.rfile.read(1):
                        break
221 222 223 224 225 226 227 228 229 230 231
                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)
232
                os.execve(scriptfile, args, os.environ)
233 234 235 236
            except:
                self.server.handle_error(self.request, self.client_address)
                os._exit(127)

237 238
        elif self.have_popen2 or self.have_popen3:
            # Windows -- use popen2 or popen3 to create a subprocess
239
            import shutil
240 241 242 243
            if self.have_popen3:
                popenx = os.popen3
            else:
                popenx = os.popen2
244 245 246 247
            cmdline = scriptfile
            if self.is_python(scriptfile):
                interp = sys.executable
                if interp.lower().endswith("w.exe"):
248 249
                    # On Windows, use python.exe, not pythonw.exe
                    interp = interp[:-5] + interp[-4:]
250
                cmdline = "%s -u %s" % (interp, cmdline)
251 252
            if '=' not in query and '"' not in query:
                cmdline = '%s "%s"' % (cmdline, query)
Guido van Rossum's avatar
Guido van Rossum committed
253
            self.log_message("command: %s", cmdline)
254 255
            try:
                nbytes = int(length)
256
            except (TypeError, ValueError):
257
                nbytes = 0
258 259 260 261 262
            files = popenx(cmdline, 'b')
            fi = files[0]
            fo = files[1]
            if self.have_popen3:
                fe = files[2]
263 264 265
            if self.command.lower() == "post" and nbytes > 0:
                data = self.rfile.read(nbytes)
                fi.write(data)
266 267
            # throw away additional data [see bug #427345]
            while select.select([self.rfile._sock], [], [], 0)[0]:
268 269
                if not self.rfile._sock.recv(1):
                    break
270 271
            fi.close()
            shutil.copyfileobj(fo, self.wfile)
272 273 274 275 276
            if self.have_popen3:
                errors = fe.read()
                fe.close()
                if errors:
                    self.log_error('%s', errors)
277
            sts = fo.close()
278
            if sts:
279
                self.log_error("CGI script exit status %#x", sts)
280
            else:
Guido van Rossum's avatar
Guido van Rossum committed
281
                self.log_message("CGI script exited OK")
282 283 284 285 286 287 288

        else:
            # Other O.S. -- execute script in this process
            save_argv = sys.argv
            save_stdin = sys.stdin
            save_stdout = sys.stdout
            save_stderr = sys.stderr
289
            try:
290
                save_cwd = os.getcwd()
291 292 293 294 295 296 297 298 299 300 301 302
                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
303
                    os.chdir(save_cwd)
304 305 306
            except SystemExit, sts:
                self.log_error("CGI script exit status %s", str(sts))
            else:
Guido van Rossum's avatar
Guido van Rossum committed
307
                self.log_message("CGI script exited OK")
Guido van Rossum's avatar
Guido van Rossum committed
308 309 310 311 312 313 314 315


nobody = None

def nobody_uid():
    """Internal routine to get nobody's uid"""
    global nobody
    if nobody:
316
        return nobody
317 318 319 320
    try:
        import pwd
    except ImportError:
        return -1
Guido van Rossum's avatar
Guido van Rossum committed
321
    try:
322
        nobody = pwd.getpwnam('nobody')[2]
323
    except KeyError:
324
        nobody = 1 + max(map(lambda x: x[2], pwd.getpwall()))
Guido van Rossum's avatar
Guido van Rossum committed
325 326 327 328 329 330
    return nobody


def executable(path):
    """Test for executable file."""
    try:
331
        st = os.stat(path)
Guido van Rossum's avatar
Guido van Rossum committed
332
    except os.error:
333
        return False
334
    return st.st_mode & 0111 != 0
Guido van Rossum's avatar
Guido van Rossum committed
335 336 337


def test(HandlerClass = CGIHTTPRequestHandler,
338
         ServerClass = BaseHTTPServer.HTTPServer):
Guido van Rossum's avatar
Guido van Rossum committed
339 340 341 342 343
    SimpleHTTPServer.test(HandlerClass, ServerClass)


if __name__ == '__main__':
    test()