1
# -*- test-case-name: twisted.web.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.
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
27
from server import NOT_DONE_YET
29
class CGIDirectory(resource.Resource, filepath.FilePath):
30
def __init__(self, pathname):
31
resource.Resource.__init__(self)
32
filepath.FilePath.__init__(self, pathname)
34
def getChild(self, path, request):
35
fnp = self.child(path)
37
return static.File.childNotFound
39
return CGIDirectory(fnp.path)
41
return CGIScript(fnp.path)
42
return error.NoResource()
44
def render(self, request):
45
return error.NoResource("CGI directories do not support directory listing.").render(request)
47
class CGIScript(resource.Resource):
48
"""I represent a CGI script.
50
My implementation is complex due to the fact that it requires asynchronous
51
IPC with an external process with an unpleasant protocol.
54
def __init__(self, filename, registry=None):
55
"""Initialize, with the name of a CGI script file.
57
self.filename = filename
59
def render(self, request):
60
"""Do various things to conform to the CGI specification.
62
I will set up the usual slew of environment variables, then spin off a
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,
79
client = request.getClient()
80
if client is not None:
81
env['REMOTE_HOST'] = client
82
ip = request.getClientIP()
84
env['REMOTE_ADDR'] = ip
87
env["PATH_INFO"] = "/"+string.join(pp, '/')
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)
97
qindex = string.find(request.uri, '?')
99
qs = env['QUERY_STRING'] = request.uri[qindex+1:]
103
qargs = [urllib.unquote(x) for x in qs.split('+')]
105
env['QUERY_STRING'] = ''
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):
119
self.runProcess(env, request, qargs)
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))
127
class FilteredScript(CGIScript):
128
"""I am a special version of a CGI script, that uses a specific executable.
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.
136
filter = '/usr/bin/cat'
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))
143
class PHP3Script(FilteredScript):
144
"""I am a FilteredScript that uses the default PHP3 command on most systems.
147
filter = '/usr/bin/php3'
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.
155
filter = '/usr/bin/php4'
158
class CGIProcessProtocol(protocol.ProcessProtocol, pb.Viewable):
164
# Remotely relay producer interface.
166
def view_resumeProducing(self, issuer):
167
self.resumeProducing()
169
def view_pauseProducing(self, issuer):
170
self.pauseProducing()
172
def view_stopProducing(self, issuer):
175
def resumeProducing(self):
176
self.transport.resumeProducing()
178
def pauseProducing(self):
179
self.transport.pauseProducing()
181
def stopProducing(self):
182
self.transport.loseConnection()
184
def __init__(self, request):
185
self.request = request
187
def connectionMade(self):
188
self.request.registerProducer(self, 1)
189
self.request.content.seek(0, 0)
190
content = self.request.content.read()
192
self.transport.write(content)
193
self.transport.closeStdin()
195
def errReceived(self, error):
196
self.errortext = self.errortext + error
198
def outReceived(self, output):
200
Handle a chunk of input
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
207
for delimiter in '\n\n','\r\n\r\n','\r\r', '\n\r\n':
208
headerend = string.find(text,delimiter)
210
headerEnds.append((headerend, delimiter))
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,': ')
221
log.msg( 'ignoring malformed CGI header: %s' % header )
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':
229
statusNum = int(headerText[:3]) #"XXX <description>" sometimes happens.
231
log.msg( "malformed status header" )
233
self.request.setResponseCode(statusNum)
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)
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))
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))
252
error.ErrorPage(http.INTERNAL_SERVER_ERROR,
254
"Premature end of script headers.").render(self.request))
255
self.request.unregisterProducer()
256
self.request.finish()