~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/web/twcgi.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.web.test.test_cgi -*-
 
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
3
# See LICENSE for details.
 
4
 
 
5
 
 
6
"""I hold resource classes and helper classes that deal with CGI scripts.
 
7
"""
 
8
 
 
9
# System Imports
 
10
import string
 
11
import os
 
12
import sys
 
13
import urllib
 
14
 
 
15
# Twisted Imports
 
16
from twisted.web import http
 
17
from twisted.internet import reactor, protocol
 
18
from twisted.spread import pb
 
19
from twisted.python import log, filepath
 
20
 
 
21
# Sibling Imports
 
22
import server
 
23
import error
 
24
import html
 
25
import resource
 
26
import static
 
27
from server import NOT_DONE_YET
 
28
 
 
29
class CGIDirectory(resource.Resource, filepath.FilePath):
 
30
    def __init__(self, pathname):
 
31
        resource.Resource.__init__(self)
 
32
        filepath.FilePath.__init__(self, pathname)
 
33
 
 
34
    def getChild(self, path, request):
 
35
        fnp = self.child(path)
 
36
        if not fnp.exists():
 
37
            return static.File.childNotFound
 
38
        elif fnp.isdir():
 
39
            return CGIDirectory(fnp.path)
 
40
        else:
 
41
            return CGIScript(fnp.path)
 
42
        return error.NoResource()
 
43
 
 
44
    def render(self, request):
 
45
        return error.NoResource("CGI directories do not support directory listing.").render(request)
 
46
 
 
47
class CGIScript(resource.Resource):
 
48
    """I represent a CGI script.
 
49
 
 
50
    My implementation is complex due to the fact that it requires asynchronous
 
51
    IPC with an external process with an unpleasant protocol.
 
52
    """
 
53
    isLeaf = 1
 
54
    def __init__(self, filename, registry=None):
 
55
        """Initialize, with the name of a CGI script file.
 
56
        """
 
57
        self.filename = filename
 
58
 
 
59
    def render(self, request):
 
60
        """Do various things to conform to the CGI specification.
 
61
 
 
62
        I will set up the usual slew of environment variables, then spin off a
 
63
        process.
 
64
        """
 
65
        script_name = "/"+string.join(request.prepath, '/')
 
66
        python_path = string.join(sys.path, os.pathsep)
 
67
        serverName = string.split(request.getRequestHostname(), ':')[0]
 
68
        env = {"SERVER_SOFTWARE":   server.version,
 
69
               "SERVER_NAME":       serverName,
 
70
               "GATEWAY_INTERFACE": "CGI/1.1",
 
71
               "SERVER_PROTOCOL":   request.clientproto,
 
72
               "SERVER_PORT":       str(request.getHost().port),
 
73
               "REQUEST_METHOD":    request.method,
 
74
               "SCRIPT_NAME":       script_name, # XXX
 
75
               "SCRIPT_FILENAME":   self.filename,
 
76
               "REQUEST_URI":       request.uri,
 
77
        }
 
78
 
 
79
        client = request.getClient()
 
80
        if client is not None:
 
81
            env['REMOTE_HOST'] = client
 
82
        ip = request.getClientIP()
 
83
        if ip is not None:
 
84
            env['REMOTE_ADDR'] = ip
 
85
        pp = request.postpath
 
86
        if pp:
 
87
            env["PATH_INFO"] = "/"+string.join(pp, '/')
 
88
 
 
89
        if hasattr(request, "content"):
 
90
            # request.content is either a StringIO or a TemporaryFile, and
 
91
            # the file pointer is sitting at the beginning (seek(0,0))
 
92
            request.content.seek(0,2)
 
93
            length = request.content.tell()
 
94
            request.content.seek(0,0)
 
95
            env['CONTENT_LENGTH'] = str(length)
 
96
 
 
97
        qindex = string.find(request.uri, '?')
 
98
        if qindex != -1:
 
99
            qs = env['QUERY_STRING'] = request.uri[qindex+1:]
 
100
            if '=' in qs:
 
101
                qargs = []
 
102
            else:
 
103
                qargs = [urllib.unquote(x) for x in qs.split('+')]
 
104
        else:
 
105
            env['QUERY_STRING'] = ''
 
106
            qargs = []
 
107
 
 
108
        # Propogate HTTP headers
 
109
        for title, header in request.getAllHeaders().items():
 
110
            envname = string.upper(string.replace(title, '-', '_'))
 
111
            if title not in ('content-type', 'content-length'):
 
112
                envname = "HTTP_" + envname
 
113
            env[envname] = header
 
114
        # Propogate our environment
 
115
        for key, value in os.environ.items():
 
116
            if not env.has_key(key):
 
117
                env[key] = value
 
118
        # And they're off!
 
119
        self.runProcess(env, request, qargs)
 
120
        return NOT_DONE_YET
 
121
 
 
122
    def runProcess(self, env, request, qargs=[]):
 
123
        p = CGIProcessProtocol(request)
 
124
        reactor.spawnProcess(p, self.filename, [self.filename]+qargs, env, os.path.dirname(self.filename))
 
125
 
 
126
 
 
127
class FilteredScript(CGIScript):
 
128
    """I am a special version of a CGI script, that uses a specific executable.
 
129
 
 
130
    This is useful for interfacing with other scripting languages that adhere
 
131
    to the CGI standard (cf. PHPScript).  My 'filter' attribute specifies what
 
132
    executable to run, and my 'filename' init parameter describes which script
 
133
    to pass to the first argument of that script.
 
134
    """
 
135
 
 
136
    filter = '/usr/bin/cat'
 
137
 
 
138
    def runProcess(self, env, request, qargs=[]):
 
139
        p = CGIProcessProtocol(request)
 
140
        reactor.spawnProcess(p, self.filter, [self.filter, self.filename]+qargs, env, os.path.dirname(self.filename))
 
141
 
 
142
 
 
143
class PHP3Script(FilteredScript):
 
144
    """I am a FilteredScript that uses the default PHP3 command on most systems.
 
145
    """
 
146
 
 
147
    filter = '/usr/bin/php3'
 
148
 
 
149
 
 
150
class PHPScript(FilteredScript):
 
151
    """I am a FilteredScript that uses the PHP command on most systems.
 
152
    Sometimes, php wants the path to itself as argv[0]. This is that time.
 
153
    """
 
154
 
 
155
    filter = '/usr/bin/php4'
 
156
 
 
157
 
 
158
class CGIProcessProtocol(protocol.ProcessProtocol, pb.Viewable):
 
159
    handling_headers = 1
 
160
    headers_written = 0
 
161
    headertext = ''
 
162
    errortext = ''
 
163
 
 
164
    # Remotely relay producer interface.
 
165
 
 
166
    def view_resumeProducing(self, issuer):
 
167
        self.resumeProducing()
 
168
 
 
169
    def view_pauseProducing(self, issuer):
 
170
        self.pauseProducing()
 
171
 
 
172
    def view_stopProducing(self, issuer):
 
173
        self.stopProducing()
 
174
 
 
175
    def resumeProducing(self):
 
176
        self.transport.resumeProducing()
 
177
 
 
178
    def pauseProducing(self):
 
179
        self.transport.pauseProducing()
 
180
 
 
181
    def stopProducing(self):
 
182
        self.transport.loseConnection()
 
183
 
 
184
    def __init__(self, request):
 
185
        self.request = request
 
186
 
 
187
    def connectionMade(self):
 
188
        self.request.registerProducer(self, 1)
 
189
        self.request.content.seek(0, 0)
 
190
        content = self.request.content.read()
 
191
        if content:
 
192
            self.transport.write(content)
 
193
        self.transport.closeStdin()
 
194
 
 
195
    def errReceived(self, error):
 
196
        self.errortext = self.errortext + error
 
197
 
 
198
    def outReceived(self, output):
 
199
        """
 
200
        Handle a chunk of input
 
201
        """
 
202
        # First, make sure that the headers from the script are sorted
 
203
        # out (we'll want to do some parsing on these later.)
 
204
        if self.handling_headers:
 
205
            text = self.headertext + output
 
206
            headerEnds = []
 
207
            for delimiter in '\n\n','\r\n\r\n','\r\r', '\n\r\n':
 
208
                headerend = string.find(text,delimiter)
 
209
                if headerend != -1:
 
210
                    headerEnds.append((headerend, delimiter))
 
211
            if headerEnds:
 
212
                headerEnds.sort()
 
213
                headerend, delimiter = headerEnds[0]
 
214
                self.headertext = text[:headerend]
 
215
                # This is a final version of the header text.
 
216
                linebreak = delimiter[:len(delimiter)/2]
 
217
                headers = string.split(self.headertext, linebreak)
 
218
                for header in headers:
 
219
                    br = string.find(header,': ')
 
220
                    if br == -1:
 
221
                        log.msg( 'ignoring malformed CGI header: %s' % header )
 
222
                    else:
 
223
                        headerName = string.lower(header[:br])
 
224
                        headerText = header[br+2:]
 
225
                        if headerName == 'location':
 
226
                            self.request.setResponseCode(http.FOUND)
 
227
                        if headerName == 'status':
 
228
                            try:
 
229
                                statusNum = int(headerText[:3]) #"XXX <description>" sometimes happens.
 
230
                            except:
 
231
                                log.msg( "malformed status header" )
 
232
                            else:
 
233
                                self.request.setResponseCode(statusNum)
 
234
                        else:
 
235
                            self.request.setHeader(headerName,headerText)
 
236
                output = text[headerend+len(delimiter):]
 
237
                self.handling_headers = 0
 
238
            if self.handling_headers:
 
239
                self.headertext = text
 
240
        if not self.handling_headers:
 
241
            self.request.write(output)
 
242
 
 
243
    def processEnded(self, reason):
 
244
        if reason.value.exitCode != 0:
 
245
            log.msg("CGI %s exited with exit code %s" %
 
246
                    (self.request.uri, reason.value.exitCode))
 
247
        if self.errortext:
 
248
            log.msg("Errors from CGI %s: %s" % (self.request.uri, self.errortext))
 
249
        if self.handling_headers:
 
250
            log.msg("Premature end of headers in %s: %s" % (self.request.uri, self.headertext))
 
251
            self.request.write(
 
252
                error.ErrorPage(http.INTERNAL_SERVER_ERROR,
 
253
                                "CGI Script Error",
 
254
                                "Premature end of script headers.").render(self.request))
 
255
        self.request.unregisterProducer()
 
256
        self.request.finish()