123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- # -*- test-case-name: twisted.web.test.test_cgi -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- I hold resource classes and helper classes that deal with CGI scripts.
- """
- # System Imports
- import os
- import urllib
- from typing import AnyStr
- # Twisted Imports
- from twisted.internet import protocol
- from twisted.logger import Logger
- from twisted.python import filepath
- from twisted.spread import pb
- from twisted.web import http, resource, server, static
- class CGIDirectory(resource.Resource, filepath.FilePath[AnyStr]):
- def __init__(self, pathname):
- resource.Resource.__init__(self)
- filepath.FilePath.__init__(self, pathname)
- def getChild(self, path, request):
- fnp = self.child(path)
- if not fnp.exists():
- return static.File.childNotFound
- elif fnp.isdir():
- return CGIDirectory(fnp.path)
- else:
- return CGIScript(fnp.path)
- def render(self, request):
- notFound = resource.NoResource(
- "CGI directories do not support directory listing."
- )
- return notFound.render(request)
- class CGIScript(resource.Resource):
- """
- L{CGIScript} is a resource which runs child processes according to the CGI
- specification.
- The implementation is complex due to the fact that it requires asynchronous
- IPC with an external process with an unpleasant protocol.
- """
- isLeaf = 1
- def __init__(self, filename, registry=None, reactor=None):
- """
- Initialize, with the name of a CGI script file.
- """
- self.filename = filename
- if reactor is None:
- # This installs a default reactor, if None was installed before.
- # We do a late import here, so that importing the current module
- # won't directly trigger installing a default reactor.
- from twisted.internet import reactor
- self._reactor = reactor
- def render(self, request):
- """
- Do various things to conform to the CGI specification.
- I will set up the usual slew of environment variables, then spin off a
- process.
- @type request: L{twisted.web.http.Request}
- @param request: An HTTP request.
- """
- scriptName = b"/" + b"/".join(request.prepath)
- serverName = request.getRequestHostname().split(b":")[0]
- env = {
- "SERVER_SOFTWARE": server.version,
- "SERVER_NAME": serverName,
- "GATEWAY_INTERFACE": "CGI/1.1",
- "SERVER_PROTOCOL": request.clientproto,
- "SERVER_PORT": str(request.getHost().port),
- "REQUEST_METHOD": request.method,
- "SCRIPT_NAME": scriptName,
- "SCRIPT_FILENAME": self.filename,
- "REQUEST_URI": request.uri,
- }
- ip = request.getClientAddress().host
- if ip is not None:
- env["REMOTE_ADDR"] = ip
- pp = request.postpath
- if pp:
- env["PATH_INFO"] = "/" + "/".join(pp)
- if hasattr(request, "content"):
- # 'request.content' is either a StringIO or a TemporaryFile, and
- # the file pointer is sitting at the beginning (seek(0,0))
- request.content.seek(0, 2)
- length = request.content.tell()
- request.content.seek(0, 0)
- env["CONTENT_LENGTH"] = str(length)
- try:
- qindex = request.uri.index(b"?")
- except ValueError:
- env["QUERY_STRING"] = ""
- qargs = []
- else:
- qs = env["QUERY_STRING"] = request.uri[qindex + 1 :]
- if b"=" in qs:
- qargs = []
- else:
- qargs = [urllib.parse.unquote(x.decode()) for x in qs.split(b"+")]
- # Propagate HTTP headers
- for title, header in request.getAllHeaders().items():
- envname = title.replace(b"-", b"_").upper()
- if title not in (b"content-type", b"content-length", b"proxy"):
- envname = b"HTTP_" + envname
- env[envname] = header
- # Propagate our environment
- for key, value in os.environ.items():
- if key not in env:
- env[key] = value
- # And they're off!
- self.runProcess(env, request, qargs)
- return server.NOT_DONE_YET
- def runProcess(self, env, request, qargs=[]):
- """
- Run the cgi script.
- @type env: A L{dict} of L{str}, or L{None}
- @param env: The environment variables to pass to the process that will
- get spawned. See
- L{twisted.internet.interfaces.IReactorProcess.spawnProcess} for
- more information about environments and process creation.
- @type request: L{twisted.web.http.Request}
- @param request: An HTTP request.
- @type qargs: A L{list} of L{str}
- @param qargs: The command line arguments to pass to the process that
- will get spawned.
- """
- p = CGIProcessProtocol(request)
- self._reactor.spawnProcess(
- p,
- self.filename,
- [self.filename] + qargs,
- env,
- os.path.dirname(self.filename),
- )
- class FilteredScript(CGIScript):
- """
- I am a special version of a CGI script, that uses a specific executable.
- This is useful for interfacing with other scripting languages that adhere
- to the CGI standard. My C{filter} attribute specifies what executable to
- run, and my C{filename} init parameter describes which script to pass to
- the first argument of that script.
- To customize me for a particular location of a CGI interpreter, override
- C{filter}.
- @type filter: L{str}
- @ivar filter: The absolute path to the executable.
- """
- filter = "/usr/bin/cat"
- def runProcess(self, env, request, qargs=[]):
- """
- Run a script through the C{filter} executable.
- @type env: A L{dict} of L{str}, or L{None}
- @param env: The environment variables to pass to the process that will
- get spawned. See
- L{twisted.internet.interfaces.IReactorProcess.spawnProcess}
- for more information about environments and process creation.
- @type request: L{twisted.web.http.Request}
- @param request: An HTTP request.
- @type qargs: A L{list} of L{str}
- @param qargs: The command line arguments to pass to the process that
- will get spawned.
- """
- p = CGIProcessProtocol(request)
- self._reactor.spawnProcess(
- p,
- self.filter,
- [self.filter, self.filename] + qargs,
- env,
- os.path.dirname(self.filename),
- )
- class CGIProcessProtocol(protocol.ProcessProtocol, pb.Viewable):
- handling_headers = 1
- headers_written = 0
- headertext = b""
- errortext = b""
- _log = Logger()
- _requestFinished = False
- # Remotely relay producer interface.
- def view_resumeProducing(self, issuer):
- self.resumeProducing()
- def view_pauseProducing(self, issuer):
- self.pauseProducing()
- def view_stopProducing(self, issuer):
- self.stopProducing()
- def resumeProducing(self):
- self.transport.resumeProducing()
- def pauseProducing(self):
- self.transport.pauseProducing()
- def stopProducing(self):
- self.transport.loseConnection()
- def __init__(self, request):
- self.request = request
- self.request.notifyFinish().addBoth(self._finished)
- def connectionMade(self):
- self.request.registerProducer(self, 1)
- self.request.content.seek(0, 0)
- content = self.request.content.read()
- if content:
- self.transport.write(content)
- self.transport.closeStdin()
- def errReceived(self, error):
- self.errortext = self.errortext + error
- def outReceived(self, output):
- """
- Handle a chunk of input
- """
- # First, make sure that the headers from the script are sorted
- # out (we'll want to do some parsing on these later.)
- if self.handling_headers:
- text = self.headertext + output
- headerEnds = []
- for delimiter in b"\n\n", b"\r\n\r\n", b"\r\r", b"\n\r\n":
- headerend = text.find(delimiter)
- if headerend != -1:
- headerEnds.append((headerend, delimiter))
- if headerEnds:
- # The script is entirely in control of response headers;
- # disable the default Content-Type value normally provided by
- # twisted.web.server.Request.
- self.request.defaultContentType = None
- headerEnds.sort()
- headerend, delimiter = headerEnds[0]
- self.headertext = text[:headerend]
- # This is a final version of the header text.
- linebreak = delimiter[: len(delimiter) // 2]
- headers = self.headertext.split(linebreak)
- for header in headers:
- br = header.find(b": ")
- if br == -1:
- self._log.error(
- "ignoring malformed CGI header: {header!r}", header=header
- )
- else:
- headerName = header[:br].lower()
- headerText = header[br + 2 :]
- if headerName == b"location":
- self.request.setResponseCode(http.FOUND)
- if headerName == b"status":
- try:
- # "XXX <description>" sometimes happens.
- statusNum = int(headerText[:3])
- except BaseException:
- self._log.error("malformed status header")
- else:
- self.request.setResponseCode(statusNum)
- else:
- # Don't allow the application to control
- # these required headers.
- if headerName.lower() not in (b"server", b"date"):
- self.request.responseHeaders.addRawHeader(
- headerName, headerText
- )
- output = text[headerend + len(delimiter) :]
- self.handling_headers = 0
- if self.handling_headers:
- self.headertext = text
- if not self.handling_headers:
- self.request.write(output)
- def processEnded(self, reason):
- if reason.value.exitCode != 0:
- self._log.error(
- "CGI {uri} exited with exit code {exitCode}",
- uri=self.request.uri,
- exitCode=reason.value.exitCode,
- )
- if self.errortext:
- self._log.error(
- "Errors from CGI {uri}: {errorText}",
- uri=self.request.uri,
- errorText=self.errortext,
- )
- if self.handling_headers:
- self._log.error(
- "Premature end of headers in {uri}: {headerText}",
- uri=self.request.uri,
- headerText=self.headertext,
- )
- if not self._requestFinished:
- self.request.write(
- resource.ErrorPage(
- http.INTERNAL_SERVER_ERROR,
- "CGI Script Error",
- "Premature end of script headers.",
- ).render(self.request)
- )
- if not self._requestFinished:
- self.request.unregisterProducer()
- self.request.finish()
- def _finished(self, ignored):
- """
- Record the end of the response generation for the request being
- serviced.
- """
- self._requestFinished = True
|