[Twisted-Python] SCGIChannel: SCGI protocol support

Neil Blakey-Milner nbm at mithrandr.moria.org
Fri Dec 20 16:40:43 EST 2002


Hi all,

SCGI (http://www.mems-exchange.org/software/scgi/) is a replacement for
the CGI protocol, in a similar manner to FCGI.  Through mod_scgi on
apache, you can delegate sections of a web site served by apache to be
handled via SCGI:

        <Location "/jobsearch">
                SCGIServer 127.0.0.1 4000
                SCGIHandler On
        </Location>

SCGIChannel serves as a replacement to HTTPChannel, performing the
request with the information given over SCGI, and returning the reply.

A couple of changes are necessary to make use of inheritance of
HTTPChannel and server.Site to work as expected with its protocol.
These are in scgi-changes.patch.

I work around the lack of these changes in the scgichannel.py attached.
Use the above apache directives and this file to see what it does.

Opinions requested.

Neil
-- 
Neil Blakey-Milner
nbm at mithrandr.moria.org
-------------- next part --------------
Index: protocols/http.py
===================================================================
RCS file: /cvs/Twisted/twisted/protocols/http.py,v
retrieving revision 1.64
diff -u -r1.64 http.py
--- protocols/http.py	3 Dec 2002 06:05:07 -0000	1.64
+++ protocols/http.py	20 Dec 2002 21:09:16 -0000
@@ -840,9 +840,9 @@
                 self.transport.loseConnection()
                 return
             command, request, version = parts
-            self.__command = command
-            self.__path = request
-            self.__version = version
+            self._command = command
+            self._path = request
+            self._version = version
             if version == 'HTTP/0.9':
                 self.allHeadersReceived()
                 self.allContentReceived()
@@ -873,15 +873,15 @@
         self.requests[-1].received_headers[header] = data
 
     def allContentReceived(self):
-        command = self.__command
-        path = self.__path
-        version = self.__version
+        command = self._command
+        path = self._path
+        version = self._version
 
         # reset ALL state variables, so we don't interfere with next request
         self.length = 0
-        self.__header = ''
+        self._header = ''
         self.__first_line = 1
-        del self.__command, self.__path, self.__version
+        del self._command, self._path, self._version
 
         req = self.requests[-1]
         req.requestReceived(command, path, version)
@@ -899,7 +899,7 @@
     def allHeadersReceived(self):
         req = self.requests[-1]
         req.parseCookies()
-        self.persistent = self.checkPersistence(req, self.__version)
+        self.persistent = self.checkPersistence(req, self._version)
         req.gotLength(self.length)
 
     def checkPersistence(self, request, version):
Index: web/server.py
===================================================================
RCS file: /cvs/Twisted/twisted/web/server.py,v
retrieving revision 1.84
diff -u -r1.84 server.py
--- web/server.py	3 Dec 2002 17:24:27 -0000	1.84
+++ web/server.py	20 Dec 2002 21:09:18 -0000
@@ -416,7 +416,7 @@
     def buildProtocol(self, addr):
         """Generate a channel attached to this site.
         """
-        channel = http.HTTPChannel()
+        channel = self.protocol()
         channel.requestFactory = Request
         channel.site = self
         channel.factory = self
-------------- next part --------------
#!/usr/local/bin/python
#
# Copyright (c) 2002 Neil Blakey-Milner
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

from twisted.protocols import http
from twisted.web import server

class SCGITransport:
    def __init__(self, realtransport):
        self.realtransport = realtransport
        self.firstline = 1

    def write(self, data):
        if self.firstline:
            data = data.replace("HTTP/1.1", "Status:")
            self.firstline = 0
        self.realtransport.write(data)

    def writeSequence(self, data):
        for entry in data:
            self.write(entry)

    def setPeer(self, peer):
        self.peer = peer

    def setHost(self, host):
        self.host = host

    def getPeer(self):
        return self.peer

    def getHost(self):
        return self.host

    def loseConnection(self):
        self.realtransport.loseConnection()

class SCGIChannel(http.HTTPChannel):
    def __init__(self):
        http.HTTPChannel.__init__(self)
        self._buffer = ""
        self._maxbufferlength = None
        self.prefix = ""

    def dataReceived(self, data):
        self._buffer = self._buffer + data
        if not self._maxbufferlength:
            idx = self._buffer.index(":")
            if not idx:
                return
            self._maxbufferlength = long(self._buffer[:idx])

        if len(self._buffer) - (idx + 2) == self._maxbufferlength:
            scgistring = self._buffer[idx + 1:]
            items = scgistring.split("\0")
            items = items[:-1]
            assert len(items) % 2 == 0, "malformed headers"
            env = {}
            for i in range(0, len(items), 2):
                env[items[i]] = items[i+1]

            request = self.requestFactory(self, len(self.requests))
            self.requests.append(request)

            if env.has_key("HTTP_USER_AGENT"):
                request.received_headers["User-Agent"] = env["HTTP_USER_AGENT"]
            if env.has_key("HTTP_ACCEPT"):
                request.received_headers["Accept"] = env["HTTP_ACCEPT"]
            if env.has_key("SERVER_NAME"):
                request.received_headers["host"] = env["SERVER_NAME"]
            if env.has_key("CONTENT_LENGTH"):
                request.received_headers["Content-Length"] = env["CONTENT_LENGTH"]

            # XXX: working around use of __version, &c. in HTTPChannel
            self._HTTPChannel__command = env["REQUEST_METHOD"]
            self._HTTPChannel__path = env["REQUEST_URI"][len(self.prefix):]
            self._HTTPChannel__version = env["SERVER_PROTOCOL"]
            self.transport.setPeer(("INET", env["REMOTE_ADDR"], int(env["REMOTE_PORT"])))
            self.transport.setHost(("INET", env["SERVER_NAME"], int(env["SERVER_PORT"])))

            self.allHeadersReceived()
            self.allContentReceived()
                
    def requestDone(self, request):
        self.transport.loseConnection()

    def connectionMade(self):
        self.transport = SCGITransport(self.transport)

class SCGISite(server.Site):
    def __init__(self, resource, prefix = ""):
        server.Site.__init__(self, resource)
        self.protocol = SCGIChannel
        self.prefix = prefix

    # XXX: Fix when twisted.web.server.Site starts using self.protocol()
    def buildProtocol(self, addr):
        """Generate a channel attached to this site.
        """
        channel = self.protocol()
        channel.requestFactory = server.Request
        channel.site = self
        channel.factory = self
        channel.prefix = self.prefix
        return channel

def main():
    from twisted.internet import reactor
    from twisted.web import resource, widgets

    class IndexDisplay(widgets.Presentation):
        template = """<h1>Index</h1>"""

    gdgt = widgets.Gadget()
    gdgt.putWidget('', IndexDisplay())
    site = SCGISite(gdgt, "/jobsearch")
    site.protocol = SCGIChannel
    reactor.listenTCP(4000, site)
    reactor.run()

if __name__ == "__main__":
    main()


More information about the Twisted-Python mailing list