Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / web / twcgi.py
1 # -*- test-case-name: twisted.web.test.test_cgi -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5
6 """
7 I hold resource classes and helper classes that deal with CGI scripts.
8 """
9
10 # System Imports
11 import string
12 import os
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 from twisted.web import resource, server, static
21
22
23 class CGIDirectory(resource.Resource, filepath.FilePath):
24     def __init__(self, pathname):
25         resource.Resource.__init__(self)
26         filepath.FilePath.__init__(self, pathname)
27
28     def getChild(self, path, request):
29         fnp = self.child(path)
30         if not fnp.exists():
31             return static.File.childNotFound
32         elif fnp.isdir():
33             return CGIDirectory(fnp.path)
34         else:
35             return CGIScript(fnp.path)
36         return resource.NoResource()
37
38     def render(self, request):
39         notFound = resource.NoResource(
40             "CGI directories do not support directory listing.")
41         return notFound.render(request)
42
43
44
45 class CGIScript(resource.Resource):
46     """
47     L{CGIScript} is a resource which runs child processes according to the CGI
48     specification.
49
50     The 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         """
56         Initialize, with the name of a CGI script file.
57         """
58         self.filename = filename
59
60
61     def render(self, request):
62         """
63         Do various things to conform to the CGI specification.
64
65         I will set up the usual slew of environment variables, then spin off a
66         process.
67
68         @type request: L{twisted.web.http.Request}
69         @param request: An HTTP request.
70         """
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,
82         }
83
84         client = request.getClient()
85         if client is not None:
86             env['REMOTE_HOST'] = client
87         ip = request.getClientIP()
88         if ip is not None:
89             env['REMOTE_ADDR'] = ip
90         pp = request.postpath
91         if pp:
92             env["PATH_INFO"] = "/"+string.join(pp, '/')
93
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)
101
102         qindex = string.find(request.uri, '?')
103         if qindex != -1:
104             qs = env['QUERY_STRING'] = request.uri[qindex+1:]
105             if '=' in qs:
106                 qargs = []
107             else:
108                 qargs = [urllib.unquote(x) for x in qs.split('+')]
109         else:
110             env['QUERY_STRING'] = ''
111             qargs = []
112
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):
122                 env[key] = value
123         # And they're off!
124         self.runProcess(env, request, qargs)
125         return server.NOT_DONE_YET
126
127
128     def runProcess(self, env, request, qargs=[]):
129         """
130         Run the cgi script.
131
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
134             get spawned. See
135             L{twisted.internet.interfaces.IReactorProcess.spawnProcess} for more
136             information about environments and process creation.
137
138         @type request: L{twisted.web.http.Request}
139         @param request: An HTTP request.
140
141         @type qargs: A C{list} of C{str}
142         @param qargs: The command line arguments to pass to the process that
143             will get spawned.
144         """
145         p = CGIProcessProtocol(request)
146         reactor.spawnProcess(p, self.filename, [self.filename] + qargs, env,
147                              os.path.dirname(self.filename))
148
149
150
151 class FilteredScript(CGIScript):
152     """
153     I am a special version of a CGI script, that uses a specific executable.
154
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.
159
160     To customize me for a particular location of a CGI interpreter, override
161     C{filter}.
162
163     @type filter: C{str}
164     @ivar filter: The absolute path to the executable.
165     """
166
167     filter = '/usr/bin/cat'
168
169
170     def runProcess(self, env, request, qargs=[]):
171         """
172         Run a script through the C{filter} executable.
173
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
176             get spawned. See
177             L{twisted.internet.interfaces.IReactorProcess.spawnProcess} for more
178             information about environments and process creation.
179
180         @type request: L{twisted.web.http.Request}
181         @param request: An HTTP request.
182
183         @type qargs: A C{list} of C{str}
184         @param qargs: The command line arguments to pass to the process that
185             will get spawned.
186         """
187         p = CGIProcessProtocol(request)
188         reactor.spawnProcess(p, self.filter,
189                              [self.filter, self.filename] + qargs, env,
190                              os.path.dirname(self.filename))
191
192
193
194 class CGIProcessProtocol(protocol.ProcessProtocol, pb.Viewable):
195     handling_headers = 1
196     headers_written = 0
197     headertext = ''
198     errortext = ''
199
200     # Remotely relay producer interface.
201
202     def view_resumeProducing(self, issuer):
203         self.resumeProducing()
204
205     def view_pauseProducing(self, issuer):
206         self.pauseProducing()
207
208     def view_stopProducing(self, issuer):
209         self.stopProducing()
210
211     def resumeProducing(self):
212         self.transport.resumeProducing()
213
214     def pauseProducing(self):
215         self.transport.pauseProducing()
216
217     def stopProducing(self):
218         self.transport.loseConnection()
219
220     def __init__(self, request):
221         self.request = request
222
223     def connectionMade(self):
224         self.request.registerProducer(self, 1)
225         self.request.content.seek(0, 0)
226         content = self.request.content.read()
227         if content:
228             self.transport.write(content)
229         self.transport.closeStdin()
230
231     def errReceived(self, error):
232         self.errortext = self.errortext + error
233
234     def outReceived(self, output):
235         """
236         Handle a chunk of input
237         """
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
242             headerEnds = []
243             for delimiter in '\n\n','\r\n\r\n','\r\r', '\n\r\n':
244                 headerend = text.find(delimiter)
245                 if headerend != -1:
246                     headerEnds.append((headerend, delimiter))
247             if headerEnds:
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
252
253                 headerEnds.sort()
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(': ')
261                     if br == -1:
262                         log.msg( 'ignoring malformed CGI header: %s' % header )
263                     else:
264                         headerName = header[:br].lower()
265                         headerText = header[br+2:]
266                         if headerName == 'location':
267                             self.request.setResponseCode(http.FOUND)
268                         if headerName == 'status':
269                             try:
270                                 statusNum = int(headerText[:3]) #"XXX <description>" sometimes happens.
271                             except:
272                                 log.msg( "malformed status header" )
273                             else:
274                                 self.request.setResponseCode(statusNum)
275                         else:
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)
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.request.write(
295                 resource.ErrorPage(http.INTERNAL_SERVER_ERROR,
296                                    "CGI Script Error",
297                                    "Premature end of script headers.").render(self.request))
298         self.request.unregisterProducer()
299         self.request.finish()