| 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 |