OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.web.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 | |
9 # System Imports | |
10 import string | |
11 import os | |
12 import sys | |
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 | |
21 # Sibling Imports | |
22 import server | |
23 import error | |
24 import html | |
25 import resource | |
26 import static | |
27 from server import NOT_DONE_YET | |
28 | |
29 class CGIDirectory(resource.Resource, filepath.FilePath): | |
30 def __init__(self, pathname): | |
31 resource.Resource.__init__(self) | |
32 filepath.FilePath.__init__(self, pathname) | |
33 | |
34 def getChild(self, path, request): | |
35 fnp = self.child(path) | |
36 if not fnp.exists(): | |
37 return static.File.childNotFound | |
38 elif fnp.isdir(): | |
39 return CGIDirectory(fnp.path) | |
40 else: | |
41 return CGIScript(fnp.path) | |
42 return error.NoResource() | |
43 | |
44 def render(self, request): | |
45 return error.NoResource("CGI directories do not support directory listin
g.").render(request) | |
46 | |
47 class CGIScript(resource.Resource): | |
48 """I represent a CGI script. | |
49 | |
50 My 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 """Initialize, with the name of a CGI script file. | |
56 """ | |
57 self.filename = filename | |
58 | |
59 def render(self, request): | |
60 """Do various things to conform to the CGI specification. | |
61 | |
62 I will set up the usual slew of environment variables, then spin off a | |
63 process. | |
64 """ | |
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, | |
77 } | |
78 | |
79 client = request.getClient() | |
80 if client is not None: | |
81 env['REMOTE_HOST'] = client | |
82 ip = request.getClientIP() | |
83 if ip is not None: | |
84 env['REMOTE_ADDR'] = ip | |
85 pp = request.postpath | |
86 if pp: | |
87 env["PATH_INFO"] = "/"+string.join(pp, '/') | |
88 | |
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) | |
96 | |
97 qindex = string.find(request.uri, '?') | |
98 if qindex != -1: | |
99 qs = env['QUERY_STRING'] = request.uri[qindex+1:] | |
100 if '=' in qs: | |
101 qargs = [] | |
102 else: | |
103 qargs = [urllib.unquote(x) for x in qs.split('+')] | |
104 else: | |
105 env['QUERY_STRING'] = '' | |
106 qargs = [] | |
107 | |
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): | |
117 env[key] = value | |
118 # And they're off! | |
119 self.runProcess(env, request, qargs) | |
120 return NOT_DONE_YET | |
121 | |
122 def runProcess(self, env, request, qargs=[]): | |
123 p = CGIProcessProtocol(request) | |
124 reactor.spawnProcess(p, self.filename, [self.filename]+qargs, env, os.pa
th.dirname(self.filename)) | |
125 | |
126 | |
127 class FilteredScript(CGIScript): | |
128 """I am a special version of a CGI script, that uses a specific executable. | |
129 | |
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. | |
134 """ | |
135 | |
136 filter = '/usr/bin/cat' | |
137 | |
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)) | |
141 | |
142 | |
143 class PHP3Script(FilteredScript): | |
144 """I am a FilteredScript that uses the default PHP3 command on most systems. | |
145 """ | |
146 | |
147 filter = '/usr/bin/php3' | |
148 | |
149 | |
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. | |
153 """ | |
154 | |
155 filter = '/usr/bin/php4' | |
156 | |
157 | |
158 class CGIProcessProtocol(protocol.ProcessProtocol, pb.Viewable): | |
159 handling_headers = 1 | |
160 headers_written = 0 | |
161 headertext = '' | |
162 errortext = '' | |
163 | |
164 # Remotely relay producer interface. | |
165 | |
166 def view_resumeProducing(self, issuer): | |
167 self.resumeProducing() | |
168 | |
169 def view_pauseProducing(self, issuer): | |
170 self.pauseProducing() | |
171 | |
172 def view_stopProducing(self, issuer): | |
173 self.stopProducing() | |
174 | |
175 def resumeProducing(self): | |
176 self.transport.resumeProducing() | |
177 | |
178 def pauseProducing(self): | |
179 self.transport.pauseProducing() | |
180 | |
181 def stopProducing(self): | |
182 self.transport.loseConnection() | |
183 | |
184 def __init__(self, request): | |
185 self.request = request | |
186 | |
187 def connectionMade(self): | |
188 self.request.registerProducer(self, 1) | |
189 self.request.content.seek(0, 0) | |
190 content = self.request.content.read() | |
191 if content: | |
192 self.transport.write(content) | |
193 self.transport.closeStdin() | |
194 | |
195 def errReceived(self, error): | |
196 self.errortext = self.errortext + error | |
197 | |
198 def outReceived(self, output): | |
199 """ | |
200 Handle a chunk of input | |
201 """ | |
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 | |
206 headerEnds = [] | |
207 for delimiter in '\n\n','\r\n\r\n','\r\r', '\n\r\n': | |
208 headerend = string.find(text,delimiter) | |
209 if headerend != -1: | |
210 headerEnds.append((headerend, delimiter)) | |
211 if headerEnds: | |
212 headerEnds.sort() | |
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,': ') | |
220 if br == -1: | |
221 log.msg( 'ignoring malformed CGI header: %s' % header ) | |
222 else: | |
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': | |
228 try: | |
229 statusNum = int(headerText[:3]) #"XXX <descripti
on>" sometimes happens. | |
230 except: | |
231 log.msg( "malformed status header" ) | |
232 else: | |
233 self.request.setResponseCode(statusNum) | |
234 else: | |
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) | |
242 | |
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)) | |
247 if self.errortext: | |
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, se
lf.headertext)) | |
251 self.request.write( | |
252 error.ErrorPage(http.INTERNAL_SERVER_ERROR, | |
253 "CGI Script Error", | |
254 "Premature end of script headers.").render(self.
request)) | |
255 self.request.unregisterProducer() | |
256 self.request.finish() | |
OLD | NEW |