durusmail: quixote-users: An SCGI client module for Twisted Web
An SCGI client module for Twisted Web
2004-07-31
2004-07-31
2004-07-31
2004-07-31
An SCGI client module for Twisted Web
Matt Campbell
2004-07-31
The problem with your solution is that your HTTP server can only pass
requests off to the SCGI server.  It would be nice if the same Twisted
Web server could handle static content itself and pass requests for
dynamic content off to the SCGI server, as Apache can.  My solution
makes this possible, though my preliminary test script doesn't configure
Twisted Web this way.

I've attached two files to this message (hoping Mailman isn't configured
to disallow attachments).  twscgi.py is the module that contains the
SCGI client implementation.  I placed this file in $HOME/scgi-1.2/scgi
so it would go in /usr/lib/python2.3/site-packages/scgi when I installed
the scgi package using distutils.  I'm not sure if the scgi package is
the right place for this module, but it'll work for now.

test-twscgi.py is a little script that sets up a Twisted Web server on
port 8000, which delegates all requests to the SCGI server on port 1984
(the port used for SCGI in Toboso's dist.py script).  Again, with
Twisted Web, more complicated configurations are possible, since the
SCGIResource class in twscgi is a subclass of
twisted.web.resource.Resource.  In my test script, the root resource is
an SCGIResource, but it doesn't have to be.

I hope this helps.

--
Matt Campbell
Lead Programmer
Serotek Corporation
www.freedombox.info

# twscgi.py:  SCGI client for Twisted Web
# by Matt Campbell , July 31, 2004
# Uses substantial chunks of code from twcgi.py
#
# Twisted, the Framework of Your Internet
# Copyright (C) 2001 Matthew W. Lefkowitz
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""I hold resource classes and helper classes that deal with SCGI servers.
"""

# System Imports
import string
import os
import sys
import urllib

# Twisted Imports
from twisted.protocols import http, basic
from twisted.internet import reactor, protocol
from twisted.spread import pb
from twisted.python import log, filepath
from twisted.web import server, error, html, resource, static, twcgi
from twisted.web.server import NOT_DONE_YET

SCGI_PROTOCOL_VERSION = "1"

class SCGIResource(twcgi.CGIScript):
    """I represent a resource provided by an SCGI server.
    """
    def __init__(self, host="127.0.0.1", port=4000, registry=None):
        self.host = host
        self.port = port
        twcgi.CGIScript.__init__(self, filename="")

    def runProcess(self, env, request, qargs=[]):
        env["SCRIPT_NAME"] = request.path
        if "PATH_INFO" in env: # make it like Apache mod_scgi
            del env["PATH_INFO"]
        factory = SCGIClientFactory(env, request)
        reactor.connectTCP(self.host, self.port, factory)


class SCGIClient(basic.LineReceiver, pb.Viewable):
    handling_headers = 1

    # 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 connectionMade(self):
        env, request = self.factory.env, self.factory.request
        contentLength = "0"
        if env.get("CONTENT_LENGTH"):
            contentLength = env["CONTENT_LENGTH"]
            print "content-length:", contentLength
            del env["CONTENT_LENGTH"]
        envstr = "CONTENT_LENGTH\x00%s\x00" % contentLength
        env["SCGI"] = SCGI_PROTOCOL_VERSION
        if "HTTP_COOKIE" in env:
            print "cookie:", env["HTTP_COOKIE"]
        envstr += "".join(["%s\x00%s\x00" % (key, value)
                           for key, value in env.items()])
        self.transport.write("%d:%s," % (len(envstr), envstr))
        request.registerProducer(self, 1)
        request.content.seek(0, 0)
        content = request.content.read()
        if content:
            print "content:", content
            self.transport.write(content)

    def lineReceived(self, line):
        # This is so much simpler than CGIProcessProtocol!
        line = line.replace("\r", "")
        if line == "":
            self.handling_headers = 0
            self.setRawMode()
        else:
            request = self.factory.request
            header = line
            br = string.find(header,': ')
            if br == -1:
                log.msg( 'ignoring malformed SCGI header: %s' % header )
            else:
                headerName = string.lower(header[:br])
                headerText = header[br+2:]
                if headerName == 'location':
                    request.setResponseCode(http.FOUND)
                if headerName == 'status':
                    try:
                        statusNum = int(headerText[:3]) #"XXX "
sometimes happens.
                    except:
                        log.msg( "malformed status header" )
                    else:
                        request.setResponseCode(statusNum)
                elif headerName == "set-cookie":
                    # HTTPRequest.addCookie() puts the cookie together given
                    # the key, value, etc.  Here we just want to pass it on
                    # as we received it.
                    request.cookies.append(headerText)
                else:
                    request.setHeader(headerName,headerText)

    def rawDataReceived(self, output):
        self.factory.request.write(output)

    def connectionLost(self, reason):
        request = self.factory.request
        if self.handling_headers:
            log.msg("Premature end of headers in %s" % (request.uri,))
            request.write(
                error.ErrorPage(http.INTERNAL_SERVER_ERROR,
                                "CGI Script Error",
                                "Premature end of script
headers.").render(request))
        request.unregisterProducer()
        request.finish()

class SCGIClientFactory(protocol.ReconnectingClientFactory):
    # XXX Currently this class will keep trying to connect indefinitely.
    # But it does use exponential backoff.
    protocol = SCGIClient

    def __init__(self, env, request):
        self.env = env
        self.request = request
        self.request.registerProducer(self, 1)

    def resumeProducing(self): pass
    def pauseProducing(self): pass

    def stopProducing(self):
        self.stopTrying()

    def buildProtocol(self, addr):
        self.request.unregisterProducer()
        return protocol.ReconnectingClientFactory.buildProtocol(self, addr)

    def clientConnectionLost(self, connector, reason):
        pass
from twisted.internet import reactor
from twisted.web import server
from scgi import twscgi

scgi = twscgi.SCGIResource("localhost", 1984)
site = server.Site(scgi)
reactor.listenTCP(8000, site)
reactor.run()
reply