1
# -*- test-case-name: twisted.web2.test.test_cgi -*-
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
"""I hold resource classes and helper classes that deal with CGI scripts.
8
Things which are still not working properly:
10
- CGIScript.render doesn't set REMOTE_ADDR or REMOTE_HOST in the
21
from twisted.internet import defer, protocol, reactor
22
from twisted.python import log, filepath
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
33
headerNameTranslation = ''.join([c.isalnum() and c.upper() or '_' for c in map(chr, range(256))])
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
40
python_path = os.pathsep.join(sys.path)
42
env = dict(os.environ)
44
if request.stream.length:
45
env["CONTENT_LENGTH"] = str(request.stream.length)
47
ctype = request.headers.getRawHeaders('content-type')
49
env["CONTENT_TYPE"] = ctype[0]
51
env["GATEWAY_INTERFACE"] = "CGI/1.1"
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
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)
64
env["SERVER_NAME"] = request.host
65
env["SERVER_PORT"] = str(request.port)
67
env["SERVER_PROTOCOL"] = "HTTP/%i.%i" % request.clientproto
68
env["SERVER_SOFTWARE"] = server.VERSION
71
# env["AUTH_TYPE"] # FIXME: add this
72
# env["REMOTE_HOST"] # possibly dns resolve?
75
# env["PATH_TRANSLATED"] # Completely worthless
76
# env["REMOTE_IDENT"] # Completely worthless
77
# env["REMOTE_USER"] # FIXME: add this
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"]
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
92
if title not in ('content-type', 'content-length',
93
'authorization', 'proxy-authorization'):
94
envname = "HTTP_" + envname
95
env[envname] = ','.join(header)
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
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:
113
qargs = [urllib.unquote(x) for x in request.querystring.split('+')]
115
if filterscript is None:
116
filterscript = filename
117
qargs = [filename] + qargs
119
qargs = [filterscript, filename] + qargs
121
proc = CGIProcessProtocol(request, d)
122
reactor.spawnProcess(proc, filterscript, qargs, env, os.path.dirname(filename))
125
class CGIScript(resource.LeafResource):
126
"""I represent a CGI script.
128
My implementation is complex due to the fact that it requires asynchronous
129
IPC with an external process with an unpleasant protocol.
132
def __init__(self, filename):
133
"""Initialize, with the name of a CGI script file.
135
self.filename = filename
136
resource.LeafResource.__init__(self)
138
def render(self, request):
139
"""Do various things to conform to the CGI specification.
141
I will set up the usual slew of environment variables, then spin off a
144
return runCGI(request, self.filename)
146
def http_POST(self, request):
147
return self.render(request)
151
class FilteredScript(CGIScript):
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).
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.
162
filters = '/usr/bin/cat',
164
def __init__(self, filename, filters=None):
165
if filters is not None:
166
self.filters = filters
167
CGIScript.__init__(self, filename)
170
def render(self, request):
171
for filterscript in self.filters:
172
if os.path.exists(filterscript):
173
return runCGI(request, self.filename, filterscript)
175
log.err(self.__class__.__name__ + ' could not find any of: ' + ', '.join(self.filters))
176
return http.Response(responsecode.INTERNAL_SERVER_ERROR)
179
class PHP3Script(FilteredScript):
180
"""I am a FilteredScript that uses the default PHP3 command on most systems.
183
filters = '/usr/bin/php3',
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.
191
filters = '/usr/bin/php4-cgi', '/usr/bin/php4'
194
class CGIProcessProtocol(protocol.ProcessProtocol):
200
def resumeProducing(self):
201
self.transport.resumeProducing()
203
def pauseProducing(self):
204
self.transport.pauseProducing()
206
def stopProducing(self):
207
self.transport.loseConnection()
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)
215
def connectionMade(self):
216
# Send input data over to the CGI script.
217
def _failedProducing(reason):
218
# If you really care.
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)
228
def errReceived(self, error):
229
self.errortext = self.errortext + error
231
def outReceived(self, output):
233
Handle a chunk of input
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
240
for delimiter in '\n\n','\r\n\r\n','\r\r', '\n\r\n':
241
headerend = fullText.find(delimiter)
243
header_endings.append((headerend, delimiter))
244
# Have we noticed the end of our headers in this chunk?
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
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)
267
def _addResponseHeader(self, header):
269
Save a header until we're ready to write our Response.
271
breakpoint = header.find(': ')
273
log.msg('ignoring malformed CGI header: %s' % header)
275
name = header.lower()[:breakpoint]
276
text = header[breakpoint+2:]
279
# "123 <description>" sometimes happens.
280
self.response.code = int(text.split(' ', 1)[0])
282
log.msg("malformed status header: %s" % header)
284
self.response.headers.addRawHeader(name, text)
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))
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)
298
def _sendResponse(self):
300
Call our deferred (from CGIScript.render) with a response.
302
# Fix up location stuff
303
loc = self.response.headers.getHeader('location')
304
if loc and self.response.code == responsecode.OK:
306
# FIXME: Do internal redirect
307
raise RuntimeError("Sorry, internal redirects not implemented yet.")
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
314
self.deferred.callback(self.response)
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.
321
@param pathname: A path to the directory that you wish to serve
322
CGI scripts from, for example /var/www/cgi-bin/
328
def __init__(self, pathname):
329
resource.Resource.__init__(self)
330
filepath.FilePath.__init__(self, pathname)
332
def locateChild(self, request, segments):
333
fnp = self.child(segments[0])
335
raise http.HTTPError(responsecode.NOT_FOUND)
337
return CGIDirectory(fnp.path), segments[1:]
339
return CGIScript(fnp.path), segments[1:]
342
def render(self, request):
343
errormsg = 'CGI directories do not support directory listing'
344
return http.Response(responsecode.FORBIDDEN)
347
__all__ = ['createCGIEnvironment', 'CGIDirectory', 'CGIScript', 'FilteredScript', 'PHP3Script', 'PHPScript']