~landscape/zope3/newer-from-ztk

« back to all changes in this revision

Viewing changes to src/twisted/web2/twcgi.py

  • Committer: Thomas Hervé
  • Date: 2009-07-08 13:52:04 UTC
  • Revision ID: thomas@canonical.com-20090708135204-df5eesrthifpylf8
Remove twisted copy

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: twisted.web2.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
 
Things which are still not working properly:
9
 
 
10
 
  - CGIScript.render doesn't set REMOTE_ADDR or REMOTE_HOST in the
11
 
    environment
12
 
 
13
 
"""
14
 
 
15
 
# System Imports
16
 
import os
17
 
import sys
18
 
import urllib
19
 
 
20
 
# Twisted Imports
21
 
from twisted.internet import defer, protocol, reactor
22
 
from twisted.python import log, filepath
23
 
 
24
 
# Sibling Imports
25
 
from twisted.web2 import http
26
 
from twisted.web2 import resource
27
 
from twisted.web2 import responsecode
28
 
from twisted.web2 import server
29
 
from twisted.web2 import static
30
 
from twisted.web2 import stream
31
 
 
32
 
 
33
 
headerNameTranslation = ''.join([c.isalnum() and c.upper() or '_' for c in map(chr, range(256))])
34
 
 
35
 
def createCGIEnvironment(request):
36
 
    # See http://hoohoo.ncsa.uiuc.edu/cgi/env.html for CGI interface spec
37
 
    # http://cgi-spec.golux.com/draft-coar-cgi-v11-03-clean.html for a better one
38
 
    remotehost = request.remoteAddr
39
 
 
40
 
    python_path = os.pathsep.join(sys.path)
41
 
    
42
 
    env = dict(os.environ)
43
 
    # MUST provide:
44
 
    if request.stream.length:
45
 
        env["CONTENT_LENGTH"] = str(request.stream.length)
46
 
    
47
 
    ctype = request.headers.getRawHeaders('content-type')
48
 
    if ctype:
49
 
        env["CONTENT_TYPE"] = ctype[0]
50
 
    
51
 
    env["GATEWAY_INTERFACE"] = "CGI/1.1"
52
 
 
53
 
    if request.postpath:
54
 
        # Should we raise an exception if this contains "/" chars?
55
 
        env["PATH_INFO"] = '/' + '/'.join(request.postpath)
56
 
    # MUST always be present, even if no query
57
 
    env["QUERY_STRING"] = request.querystring
58
 
    
59
 
    env["REMOTE_ADDR"] = remotehost.host
60
 
    env["REQUEST_METHOD"] = request.method
61
 
    # Should we raise an exception if this contains "/" chars?
62
 
    env["SCRIPT_NAME"] = '/' + '/'.join(request.prepath)
63
 
    
64
 
    env["SERVER_NAME"] = request.host
65
 
    env["SERVER_PORT"] = str(request.port)
66
 
    
67
 
    env["SERVER_PROTOCOL"] = "HTTP/%i.%i" % request.clientproto
68
 
    env["SERVER_SOFTWARE"] = server.VERSION
69
 
    
70
 
    # SHOULD provide
71
 
    # env["AUTH_TYPE"] # FIXME: add this
72
 
    # env["REMOTE_HOST"] # possibly dns resolve?
73
 
    
74
 
    # MAY provide
75
 
    # env["PATH_TRANSLATED"] # Completely worthless
76
 
    # env["REMOTE_IDENT"] # Completely worthless
77
 
    # env["REMOTE_USER"] # FIXME: add this
78
 
    
79
 
    # Unofficial, but useful and expected by applications nonetheless
80
 
    env["REMOTE_PORT"] = str(remotehost.port)
81
 
    env["REQUEST_SCHEME"] = request.scheme
82
 
    env["REQUEST_URI"] = request.uri
83
 
    env["HTTPS"] = ("off", "on")[request.scheme=="https"]
84
 
    env["SERVER_PORT_SECURE"] = ("0", "1")[request.scheme=="https"]
85
 
    
86
 
    # Propagate HTTP headers
87
 
    for title, header in request.headers.getAllRawHeaders():
88
 
        envname = title.translate(headerNameTranslation)
89
 
        # Don't send headers we already sent otherwise, and don't
90
 
        # send authorization headers, because that's a security
91
 
        # issue.
92
 
        if title not in ('content-type', 'content-length',
93
 
                         'authorization', 'proxy-authorization'):
94
 
            envname = "HTTP_" + envname
95
 
        env[envname] = ','.join(header)
96
 
 
97
 
    for k,v in env.items():
98
 
        if type(k) is not str:
99
 
            print "is not string:",k
100
 
        if type(v) is not str:
101
 
            print k, "is not string:",v
102
 
    return env
103
 
 
104
 
def runCGI(request, filename, filterscript=None):
105
 
    # Make sure that we don't have an unknown content-length
106
 
    if request.stream.length is None:
107
 
        return http.Response(responsecode.LENGTH_REQUIRED)
108
 
    env = createCGIEnvironment(request)
109
 
    env['SCRIPT_FILENAME'] = filename
110
 
    if '=' in request.querystring:
111
 
        qargs = []
112
 
    else:
113
 
        qargs = [urllib.unquote(x) for x in request.querystring.split('+')]
114
 
    
115
 
    if filterscript is None:
116
 
        filterscript = filename
117
 
        qargs = [filename] + qargs
118
 
    else:
119
 
        qargs = [filterscript, filename] + qargs
120
 
    d = defer.Deferred()
121
 
    proc = CGIProcessProtocol(request, d)
122
 
    reactor.spawnProcess(proc, filterscript, qargs, env, os.path.dirname(filename))
123
 
    return d
124
 
 
125
 
class CGIScript(resource.LeafResource):
126
 
    """I represent a CGI script.
127
 
 
128
 
    My implementation is complex due to the fact that it requires asynchronous
129
 
    IPC with an external process with an unpleasant protocol.
130
 
    """
131
 
    
132
 
    def __init__(self, filename):
133
 
        """Initialize, with the name of a CGI script file.
134
 
        """
135
 
        self.filename = filename
136
 
        resource.LeafResource.__init__(self)
137
 
 
138
 
    def render(self, request):
139
 
        """Do various things to conform to the CGI specification.
140
 
 
141
 
        I will set up the usual slew of environment variables, then spin off a
142
 
        process.
143
 
        """
144
 
        return runCGI(request, self.filename)
145
 
 
146
 
    def http_POST(self, request):
147
 
        return self.render(request)
148
 
 
149
 
 
150
 
 
151
 
class FilteredScript(CGIScript):
152
 
    """
153
 
    I am a special version of a CGI script, that uses a specific executable
154
 
    (or, the first existing executable in a list of executables).
155
 
 
156
 
    This is useful for interfacing with other scripting languages that adhere
157
 
    to the CGI standard (cf. PHPScript).  My 'filters' attribute specifies what
158
 
    executables to try to run, and my 'filename' init parameter describes which script
159
 
    to pass to the first argument of that script.
160
 
    """
161
 
 
162
 
    filters = '/usr/bin/cat',
163
 
 
164
 
    def __init__(self, filename, filters=None):
165
 
        if filters is not None:
166
 
            self.filters = filters
167
 
        CGIScript.__init__(self, filename)
168
 
        
169
 
 
170
 
    def render(self, request):
171
 
        for filterscript in self.filters:
172
 
            if os.path.exists(filterscript):
173
 
                return runCGI(request, self.filename, filterscript)
174
 
            else:
175
 
                log.err(self.__class__.__name__ + ' could not find any of: ' + ', '.join(self.filters))
176
 
                return http.Response(responsecode.INTERNAL_SERVER_ERROR)
177
 
 
178
 
 
179
 
class PHP3Script(FilteredScript):
180
 
    """I am a FilteredScript that uses the default PHP3 command on most systems.
181
 
    """
182
 
 
183
 
    filters = '/usr/bin/php3',
184
 
 
185
 
 
186
 
class PHPScript(FilteredScript):
187
 
    """I am a FilteredScript that uses the PHP command on most systems.
188
 
    Sometimes, php wants the path to itself as argv[0]. This is that time.
189
 
    """
190
 
 
191
 
    filters = '/usr/bin/php4-cgi', '/usr/bin/php4'
192
 
 
193
 
 
194
 
class CGIProcessProtocol(protocol.ProcessProtocol):
195
 
    handling_headers = 1
196
 
    headers_written = 0
197
 
    headertext = ''
198
 
    errortext = ''
199
 
 
200
 
    def resumeProducing(self):
201
 
        self.transport.resumeProducing()
202
 
 
203
 
    def pauseProducing(self):
204
 
        self.transport.pauseProducing()
205
 
 
206
 
    def stopProducing(self):
207
 
        self.transport.loseConnection()
208
 
 
209
 
    def __init__(self, request, deferred):
210
 
        self.request = request
211
 
        self.deferred = deferred
212
 
        self.stream = stream.ProducerStream()
213
 
        self.response = http.Response(stream=self.stream)
214
 
 
215
 
    def connectionMade(self):
216
 
        # Send input data over to the CGI script.
217
 
        def _failedProducing(reason):
218
 
            # If you really care.
219
 
            #log.err(reason)
220
 
            pass
221
 
        def _finishedProducing(result):
222
 
            self.transport.closeChildFD(0)
223
 
        s = stream.StreamProducer(self.request.stream)
224
 
        producingDeferred = s.beginProducing(self.transport)
225
 
        producingDeferred.addCallback(_finishedProducing)
226
 
        producingDeferred.addErrback(_failedProducing)
227
 
 
228
 
    def errReceived(self, error):
229
 
        self.errortext = self.errortext + error
230
 
 
231
 
    def outReceived(self, output):
232
 
        """
233
 
        Handle a chunk of input
234
 
        """
235
 
        # First, make sure that the headers from the script are sorted
236
 
        # out (we'll want to do some parsing on these later.)
237
 
        if self.handling_headers:
238
 
            fullText = self.headertext + output
239
 
            header_endings = []
240
 
            for delimiter in '\n\n','\r\n\r\n','\r\r', '\n\r\n':
241
 
                headerend = fullText.find(delimiter)
242
 
                if headerend != -1:
243
 
                    header_endings.append((headerend, delimiter))
244
 
            # Have we noticed the end of our headers in this chunk?
245
 
            if header_endings:
246
 
                header_endings.sort()
247
 
                headerend, delimiter = header_endings[0]
248
 
                # This is a final version of the header text.
249
 
                self.headertext = fullText[:headerend]
250
 
                linebreak = delimiter[:len(delimiter)/2]
251
 
                # Write all our headers to self.response
252
 
                for header in self.headertext.split(linebreak):
253
 
                    self._addResponseHeader(header)
254
 
                output = fullText[headerend+len(delimiter):]
255
 
                self.handling_headers = 0
256
 
                # Trigger our callback with a response
257
 
                self._sendResponse()
258
 
            # If we haven't hit the end of our headers yet, then
259
 
            # everything we've seen so far is _still_ headers
260
 
            if self.handling_headers:
261
 
                self.headertext = fullText
262
 
        # If we've stopped handling headers at this point, write
263
 
        # whatever output we've got.
264
 
        if not self.handling_headers:
265
 
            self.stream.write(output)
266
 
 
267
 
    def _addResponseHeader(self, header):
268
 
        """
269
 
        Save a header until we're ready to write our Response.
270
 
        """
271
 
        breakpoint = header.find(': ')
272
 
        if breakpoint == -1:
273
 
            log.msg('ignoring malformed CGI header: %s' % header)
274
 
        else:
275
 
            name = header.lower()[:breakpoint]
276
 
            text = header[breakpoint+2:]
277
 
            if name == 'status':
278
 
                try:
279
 
                     # "123 <description>" sometimes happens.
280
 
                    self.response.code = int(text.split(' ', 1)[0])
281
 
                except:
282
 
                    log.msg("malformed status header: %s" % header)
283
 
            else:
284
 
                self.response.headers.addRawHeader(name, text)
285
 
 
286
 
    def processEnded(self, reason):
287
 
        if reason.value.exitCode != 0:
288
 
            log.msg("CGI %s exited with exit code %s" %
289
 
                    (self.request.uri, reason.value.exitCode))
290
 
        if self.errortext:
291
 
            log.msg("Errors from CGI %s: %s" % (self.request.uri, self.errortext))
292
 
        if self.handling_headers:
293
 
            log.msg("Premature end of headers in %s: %s" % (self.request.uri, self.headertext))
294
 
            self.response = http.Response(responsecode.INTERNAL_SERVER_ERROR)
295
 
            self._sendResponse()
296
 
        self.stream.finish()
297
 
 
298
 
    def _sendResponse(self):
299
 
        """
300
 
        Call our deferred (from CGIScript.render) with a response.
301
 
        """
302
 
        # Fix up location stuff
303
 
        loc = self.response.headers.getHeader('location')
304
 
        if loc and self.response.code == responsecode.OK:
305
 
            if loc[0] == '/':
306
 
                # FIXME: Do internal redirect
307
 
                raise RuntimeError("Sorry, internal redirects not implemented yet.")
308
 
            else:
309
 
                # NOTE: if a script wants to output its own redirect body,
310
 
                # it must specify Status: 302 itself.
311
 
                self.response.code = 302
312
 
                self.response.stream = None
313
 
            
314
 
        self.deferred.callback(self.response)
315
 
 
316
 
 
317
 
class CGIDirectory(resource.Resource, filepath.FilePath):
318
 
    """A directory that serves only CGI scripts (to infinite depth)
319
 
    and does not support directory listings.
320
 
 
321
 
    @param pathname: A path to the directory that you wish to serve
322
 
                     CGI scripts from, for example /var/www/cgi-bin/
323
 
    @type pathname: str
324
 
    """
325
 
    
326
 
    addSlash = True
327
 
    
328
 
    def __init__(self, pathname):
329
 
        resource.Resource.__init__(self)
330
 
        filepath.FilePath.__init__(self, pathname)
331
 
 
332
 
    def locateChild(self, request, segments):
333
 
        fnp = self.child(segments[0])
334
 
        if not fnp.exists():
335
 
            raise http.HTTPError(responsecode.NOT_FOUND)
336
 
        elif fnp.isdir():
337
 
            return CGIDirectory(fnp.path), segments[1:]
338
 
        else:
339
 
            return CGIScript(fnp.path), segments[1:]
340
 
        return None, ()
341
 
 
342
 
    def render(self, request):
343
 
        errormsg = 'CGI directories do not support directory listing'
344
 
        return http.Response(responsecode.FORBIDDEN)
345
 
 
346
 
 
347
 
__all__ = ['createCGIEnvironment', 'CGIDirectory', 'CGIScript', 'FilteredScript', 'PHP3Script', 'PHPScript']