1 # -*- test-case-name: twisted.web.test.test_cgi -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
7 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
20 from twisted.web import resource, server, static
23 class CGIDirectory(resource.Resource, filepath.FilePath):
24 def __init__(self, pathname):
25 resource.Resource.__init__(self)
26 filepath.FilePath.__init__(self, pathname)
28 def getChild(self, path, request):
29 fnp = self.child(path)
31 return static.File.childNotFound
33 return CGIDirectory(fnp.path)
35 return CGIScript(fnp.path)
36 return resource.NoResource()
38 def render(self, request):
39 notFound = resource.NoResource(
40 "CGI directories do not support directory listing.")
41 return notFound.render(request)
45 class CGIScript(resource.Resource):
47 L{CGIScript} is a resource which runs child processes according to the CGI
50 The 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):
56 Initialize, with the name of a CGI script file.
58 self.filename = filename
61 def render(self, request):
63 Do various things to conform to the CGI specification.
65 I will set up the usual slew of environment variables, then spin off a
68 @type request: L{twisted.web.http.Request}
69 @param request: An HTTP request.
71 script_name = "/"+string.join(request.prepath, '/')
72 serverName = string.split(request.getRequestHostname(), ':')[0]
73 env = {"SERVER_SOFTWARE": server.version,
74 "SERVER_NAME": serverName,
75 "GATEWAY_INTERFACE": "CGI/1.1",
76 "SERVER_PROTOCOL": request.clientproto,
77 "SERVER_PORT": str(request.getHost().port),
78 "REQUEST_METHOD": request.method,
79 "SCRIPT_NAME": script_name, # XXX
80 "SCRIPT_FILENAME": self.filename,
81 "REQUEST_URI": request.uri,
84 client = request.getClient()
85 if client is not None:
86 env['REMOTE_HOST'] = client
87 ip = request.getClientIP()
89 env['REMOTE_ADDR'] = ip
92 env["PATH_INFO"] = "/"+string.join(pp, '/')
94 if hasattr(request, "content"):
95 # request.content is either a StringIO or a TemporaryFile, and
96 # the file pointer is sitting at the beginning (seek(0,0))
97 request.content.seek(0,2)
98 length = request.content.tell()
99 request.content.seek(0,0)
100 env['CONTENT_LENGTH'] = str(length)
102 qindex = string.find(request.uri, '?')
104 qs = env['QUERY_STRING'] = request.uri[qindex+1:]
108 qargs = [urllib.unquote(x) for x in qs.split('+')]
110 env['QUERY_STRING'] = ''
113 # Propogate HTTP headers
114 for title, header in request.getAllHeaders().items():
115 envname = string.upper(string.replace(title, '-', '_'))
116 if title not in ('content-type', 'content-length'):
117 envname = "HTTP_" + envname
118 env[envname] = header
119 # Propogate our environment
120 for key, value in os.environ.items():
121 if not env.has_key(key):
124 self.runProcess(env, request, qargs)
125 return server.NOT_DONE_YET
128 def runProcess(self, env, request, qargs=[]):
132 @type env: A C{dict} of C{str}, or C{None}
133 @param env: The environment variables to pass to the processs that will
135 L{twisted.internet.interfaces.IReactorProcess.spawnProcess} for more
136 information about environments and process creation.
138 @type request: L{twisted.web.http.Request}
139 @param request: An HTTP request.
141 @type qargs: A C{list} of C{str}
142 @param qargs: The command line arguments to pass to the process that
145 p = CGIProcessProtocol(request)
146 reactor.spawnProcess(p, self.filename, [self.filename] + qargs, env,
147 os.path.dirname(self.filename))
151 class FilteredScript(CGIScript):
153 I am a special version of a CGI script, that uses a specific executable.
155 This is useful for interfacing with other scripting languages that adhere to
156 the CGI standard. My C{filter} attribute specifies what executable to run,
157 and my C{filename} init parameter describes which script to pass to the
158 first argument of that script.
160 To customize me for a particular location of a CGI interpreter, override
164 @ivar filter: The absolute path to the executable.
167 filter = '/usr/bin/cat'
170 def runProcess(self, env, request, qargs=[]):
172 Run a script through the C{filter} executable.
174 @type env: A C{dict} of C{str}, or C{None}
175 @param env: The environment variables to pass to the processs that will
177 L{twisted.internet.interfaces.IReactorProcess.spawnProcess} for more
178 information about environments and process creation.
180 @type request: L{twisted.web.http.Request}
181 @param request: An HTTP request.
183 @type qargs: A C{list} of C{str}
184 @param qargs: The command line arguments to pass to the process that
187 p = CGIProcessProtocol(request)
188 reactor.spawnProcess(p, self.filter,
189 [self.filter, self.filename] + qargs, env,
190 os.path.dirname(self.filename))
194 class CGIProcessProtocol(protocol.ProcessProtocol, pb.Viewable):
200 # Remotely relay producer interface.
202 def view_resumeProducing(self, issuer):
203 self.resumeProducing()
205 def view_pauseProducing(self, issuer):
206 self.pauseProducing()
208 def view_stopProducing(self, issuer):
211 def resumeProducing(self):
212 self.transport.resumeProducing()
214 def pauseProducing(self):
215 self.transport.pauseProducing()
217 def stopProducing(self):
218 self.transport.loseConnection()
220 def __init__(self, request):
221 self.request = request
223 def connectionMade(self):
224 self.request.registerProducer(self, 1)
225 self.request.content.seek(0, 0)
226 content = self.request.content.read()
228 self.transport.write(content)
229 self.transport.closeStdin()
231 def errReceived(self, error):
232 self.errortext = self.errortext + error
234 def outReceived(self, output):
236 Handle a chunk of input
238 # First, make sure that the headers from the script are sorted
239 # out (we'll want to do some parsing on these later.)
240 if self.handling_headers:
241 text = self.headertext + output
243 for delimiter in '\n\n','\r\n\r\n','\r\r', '\n\r\n':
244 headerend = text.find(delimiter)
246 headerEnds.append((headerend, delimiter))
248 # The script is entirely in control of response headers; disable the
249 # default Content-Type value normally provided by
250 # twisted.web.server.Request.
251 self.request.defaultContentType = None
254 headerend, delimiter = headerEnds[0]
255 self.headertext = text[:headerend]
256 # This is a final version of the header text.
257 linebreak = delimiter[:len(delimiter)/2]
258 headers = self.headertext.split(linebreak)
259 for header in headers:
260 br = header.find(': ')
262 log.msg( 'ignoring malformed CGI header: %s' % header )
264 headerName = header[:br].lower()
265 headerText = header[br+2:]
266 if headerName == 'location':
267 self.request.setResponseCode(http.FOUND)
268 if headerName == 'status':
270 statusNum = int(headerText[:3]) #"XXX <description>" sometimes happens.
272 log.msg( "malformed status header" )
274 self.request.setResponseCode(statusNum)
276 # Don't allow the application to control these required headers.
277 if headerName.lower() not in ('server', 'date'):
278 self.request.responseHeaders.addRawHeader(headerName, headerText)
279 output = text[headerend+len(delimiter):]
280 self.handling_headers = 0
281 if self.handling_headers:
282 self.headertext = text
283 if not self.handling_headers:
284 self.request.write(output)
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))
295 resource.ErrorPage(http.INTERNAL_SERVER_ERROR,
297 "Premature end of script headers.").render(self.request))
298 self.request.unregisterProducer()
299 self.request.finish()