#!/usr/bin/python ''' FcgiWsgiAdapter.py serves wsgi requests over fcgi. Basic usage in wsgi script: from FcgiWsgiAdapter import serve serve(myWsgiApp) The fcgi part of this was adopted wholesale, with minor modifications, from Robin Dunn's fastcgi module; the original preamble of that module is further down. --- To use with django and mod_fcgid, you need a cgi script, say django.cgi: #!/usr/bin/python from FcgiWsgiAdapter import list_environment, serve_wsgi import os, sys os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' # or whatever from django.core.handlers.wsgi import WSGIHandler # use this to test that fastcgi works and to inspect the environment # serve_wsgi(list_environment) # use this to serve django serve_wsgi(WSGIHandler()) ----- Apache config example: RewriteRule ^/(~[^/]+)/django/(.*)$ /$1/django/django.cgi/$2 [PT] Options ExecCGI AddHandler fcgid-script .cgi # AddHandler cgi-script .cgi This would invoke django for any "/~joe/django/joes_app" request. This configuration example also works with suexec, which would be useful in a multi-user context. Commenting out fcgid-script and uncommenting cgi-script (and a server restart) will switch the execution mode from fastcgi to plain cgi, without any further changes being required anywhere else. For simpler urls when serving a single site, you could do RewriteRule ^/mysite/(.*)$ /~fakeuser/django/django.cgi/$1 [PT] Options ExecCGI AddHandler fcgid-script .cgi This config would still give you suexec in a typical apache install, since [PT] applies the output of mod_rewrite to suexec and ~fakeuser is a user directory, which suexec likes. ''' # # Copyright (c) 1998 by Total Control Software # All Rights Reserved # # # Module Name: fcgi.py # # Description: Handles communication with the FastCGI module of the # web server without using the FastCGI developers kit, but # will also work in a non-FastCGI environment, (straight CGI.) # This module was originally fetched from someplace on the # Net (I don't remember where and I can't find it now...) and # has been significantly modified to fix several bugs, be more # readable, more robust at handling large CGI data and return # document sizes, and also to fit the model that we had previously # used for FastCGI. # # WARNING: If you don't know what you are doing, don't tinker with this # module! # # Creation Date: 1/30/98 2:59:04PM # # License: This is free software. You may use this software for any # purpose including modification/redistribution, so long as # this header remains intact and that you do not claim any # rights of ownership or authorship of this software. This # software has been tested, but no warranty is expressed or # implied. # # # minor modifications by Michael Palmer: # - reduce string copying operations when writing output # - some updates to more recent python syntax import os, sys, string, socket, errno, traceback from cStringIO import StringIO from wsgiref.handlers import BaseCGIHandler # # Set various FastCGI constants # Maximum number of requests that can be handled FCGI_MAX_REQS=1 FCGI_MAX_CONNS = 1 # Supported version of the FastCGI protocol FCGI_VERSION_1 = 1 # Boolean: can this application multiplex connections? FCGI_MPXS_CONNS=0 # Record types FCGI_BEGIN_REQUEST = 1 ; FCGI_ABORT_REQUEST = 2 ; FCGI_END_REQUEST = 3 FCGI_PARAMS = 4 ; FCGI_STDIN = 5 ; FCGI_STDOUT = 6 FCGI_STDERR = 7 ; FCGI_DATA = 8 ; FCGI_GET_VALUES = 9 FCGI_GET_VALUES_RESULT = 10 FCGI_UNKNOWN_TYPE = 11 FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE # Types of management records ManagementTypes = [FCGI_GET_VALUES] FCGI_NULL_REQUEST_ID=0 # Masks for flags component of FCGI_BEGIN_REQUEST FCGI_KEEP_CONN = 1 # Values for role component of FCGI_BEGIN_REQUEST FCGI_RESPONDER = 1 ; FCGI_AUTHORIZER = 2 ; FCGI_FILTER = 3 # Values for protocolStatus component of FCGI_END_REQUEST FCGI_REQUEST_COMPLETE = 0 # Request completed nicely FCGI_CANT_MPX_CONN = 1 # This app can't multiplex FCGI_OVERLOADED = 2 # New request rejected; too busy FCGI_UNKNOWN_ROLE = 3 # Role value not known # chunk size for writing data CHUNK_SIZE = 2**13 # 8 kB - tried 64 kB but it fucks up. 8 kB is fast enough. error = 'fcgi.error' # The following function is used during debugging; it isn't called # anywhere at the moment def error(msg): "Append a string to /tmp/err" errf=open('/tmp/err', 'a+') errf.write(msg +'\n') errf.close() class record: "Class representing FastCGI records" def __init__(self): self.version = FCGI_VERSION_1 self.recType = FCGI_UNKNOWN_TYPE self.reqId = FCGI_NULL_REQUEST_ID self.content = "" # def readRecord(self, sock): s = map(ord, sock.recv(8)) self.version, self.recType, paddingLength = s[0], s[1], s[6] self.reqId, contentLength = (s[2]<<8)+s[3], (s[4]<<8)+s[5] self.content = "" while len(self.content) < contentLength: data = sock.recv(contentLength - len(self.content)) self.content = self.content + data if paddingLength != 0: padding = sock.recv(paddingLength) # Parse the content information c = self.content if self.recType == FCGI_BEGIN_REQUEST: self.role = (ord(c[0])<<8) + ord(c[1]) self.flags = ord(c[2]) elif self.recType == FCGI_UNKNOWN_TYPE: self.unknownType = ord(c[0]) elif self.recType == FCGI_GET_VALUES or self.recType == FCGI_PARAMS: self.values={} pos=0 while pos < len(c): name, value, pos = readPair(c, pos) self.values[name] = value elif self.recType == FCGI_END_REQUEST: b = map(ord, c[0:4]) self.appStatus = (b[0]<<24) + (b[1]<<16) + (b[2]<<8) + b[3] self.protocolStatus = ord(c[4]) # def writeRecord(self, sock): content = self.content if self.recType == FCGI_BEGIN_REQUEST: content = chr(self.role>>8) + chr(self.role & 255) + chr(self.flags) + 5*'\000' elif self.recType == FCGI_UNKNOWN_TYPE: content = chr(self.unknownType) + 7*'\000' elif self.recType==FCGI_GET_VALUES or self.recType==FCGI_PARAMS: content = "" for i in self.values.keys(): content = content + writePair(i, self.values[i]) elif self.recType==FCGI_END_REQUEST: v = self.appStatus content = chr((v>>24)&255) + chr((v>>16)&255) + chr((v>>8)&255) + chr(v&255) content = content + chr(self.protocolStatus) + 3*'\000' cLen = len(content) eLen = (cLen + 7) & (0xFFFF - 7) # align to an 8-byte boundary padLen = eLen - cLen hdr = [ self.version, self.recType, self.reqId >> 8, self.reqId & 255, cLen >> 8, cLen & 255, padLen, 0] # hdr = string.joinfields(map(chr, hdr), '') hdr = ''.join([chr(h) for h in hdr]) sock.send(hdr + content + padLen*'\000') # def readPair(s, pos): nameLen = ord(s[pos]) pos += 1 if nameLen & 128: b = [ord(x) for x in s[pos:pos+3]] pos += 3 nameLen = ((nameLen&127)<<24) + (b[0]<<16) + (b[1]<<8) + b[2] valueLen = ord(s[pos]) pos += 1 if valueLen & 128: b = [ord(x) for x in s[pos:pos+3]] pos += 3 valueLen=((valueLen&127)<<24) + (b[0]<<16) + (b[1]<<8) + b[2] return ( s[pos:pos+nameLen], s[pos+nameLen:pos+nameLen+valueLen], pos+nameLen+valueLen ) def writePair(name, value): l=len(name) if l<128: s=chr(l) else: s=chr(128|(l>>24)&255) + chr((l>>16)&255) + chr((l>>8)&255) + chr(l&255) l=len(value) if l<128: s=s+chr(l) else: s=s+chr(128|(l>>24)&255) + chr((l>>16)&255) + chr((l>>8)&255) + chr(l&255) return s + name + value # def HandleManTypes(r, conn): if r.recType == FCGI_GET_VALUES: r.recType = FCGI_GET_VALUES_RESULT v={} vars={'FCGI_MAX_CONNS' : FCGI_MAX_CONNS, 'FCGI_MAX_REQS' : FCGI_MAX_REQS, 'FCGI_MPXS_CONNS': FCGI_MPXS_CONNS} for i in r.values.keys(): if vars.has_key(i): v[i]=vars[i] r.values=vars r.writeRecord(conn) # # _isFCGI = 1 # assume it is until we find out for sure def isFCGI(): global _isFCGI return _isFCGI # fcgi_init = None _sock = None class FCGI: def __init__(self): self.haveFinished = 0 if fcgi_init == None: _startup() if not isFCGI(): self.haveFinished = 1 self.inp, self.out, self.err, self.env = \ sys.stdin, sys.stdout, sys.stderr, os.environ return if os.environ.has_key('FCGI_WEB_SERVER_ADDRS'): good_addrs=string.split(os.environ['FCGI_WEB_SERVER_ADDRS'], ',') good_addrs=map(string.strip(good_addrs)) # Remove whitespace else: good_addrs=None self.conn, addr=_sock.accept() stdin, data="", "" self.env = {} self.requestId=0 remaining=1 # Check if the connection is from a legal address if good_addrs!=None and addr not in good_addrs: raise error, 'Connection from invalid server!' while remaining: r = record() r.readRecord(self.conn) if r.recType in ManagementTypes: HandleManTypes(r, self.conn) elif r.reqId==0: # Oh, poopy. It's a management record of an unknown # type. Signal the error. r2 = record() r2.recType = FCGI_UNKNOWN_TYPE ; r2.unknownType=r.recType r2.writeRecord(self.conn) continue # Charge onwards # Ignore requests that aren't active elif r.reqId != self.requestId and r.recType != FCGI_BEGIN_REQUEST: continue # If we're already doing a request, ignore further BEGIN_REQUESTs elif r.recType == FCGI_BEGIN_REQUEST and self.requestId != 0: continue # Begin a new request if r.recType == FCGI_BEGIN_REQUEST: self.requestId = r.reqId if r.role == FCGI_AUTHORIZER: remaining=1 elif r.role == FCGI_RESPONDER: remaining=2 elif r.role == FCGI_FILTER: remaining=3 elif r.recType == FCGI_PARAMS: if r.content == "": remaining=remaining-1 else: for i in r.values.keys(): self.env[i] = r.values[i] elif r.recType == FCGI_STDIN: if r.content == "": remaining=remaining-1 else: stdin=stdin+r.content elif r.recType==FCGI_DATA: if r.content == "": remaining=remaining-1 else: data=data+r.content # end of while remaining: self.inp = sys.stdin = StringIO(stdin) self.err = sys.stderr = StringIO() self.out = sys.stdout = StringIO() #self.data = StringIO(data) #def __del__(self): I really don't get what this is good for... #self.finish() def finish(self, status=0): if not self.haveFinished: self.haveFinished = 1 self.err.seek(0,0) self.out.seek(0,0) r=record() r.recType = FCGI_STDERR r.reqId = self.requestId data = self.err.read() chunker = self.datachunker(data) for chunk in chunker: r.content = chunk r.writeRecord(self.conn) r.content="" ; r.writeRecord(self.conn) # Terminate stream r.recType = FCGI_STDOUT data = self.out.read() chunker = self.datachunker(data) for chunk in chunker: r.content = chunk r.writeRecord(self.conn) r.content="" ; r.writeRecord(self.conn) # Terminate stream r=record() r.recType=FCGI_END_REQUEST r.reqId=self.requestId r.appStatus=status r.protocolStatus=FCGI_REQUEST_COMPLETE r.writeRecord(self.conn) self.conn.close() elif not isFCGI(): # for some reason cgi repeats again and again if we don't do this. sys.exit() def datachunker(self, data): ''' yield string in chunks for writing ''' c = 0 cs = CHUNK_SIZE d = data[c:c + cs] if d: c += cs yield d else: raise StopIteration def _startup(): global fcgi_init fcgi_init = 1 try: s=socket.fromfd(sys.stdin.fileno(), socket.AF_INET, socket.SOCK_STREAM) s.getpeername() except socket.error, (err, errmsg): if err != errno.ENOTCONN: # must be a non-fastCGI environment global _isFCGI _isFCGI = 0 return global _sock _sock = s #-------------------------------------------------------------------- # the code below is not part of the original fcgi.py module; it builds # on the fcgi.py code to run wsgi applications. # show full traceback if request originated from any of these ips SHOW_TRACEBACK_IPS = ['127.0.0.1'] class FCGIHandler(BaseCGIHandler): ''' handle a single request received through fcgi ''' wsgi_multithread = False # the underlying fcgi module is not threaded wsgi_multiprocess = True # might be wrong, depending on your config wsgi_run_once = False # ... likewise origin_server = False # We are not transmitting direct to client, so won't # send http status line and protocol information def __init__(self, request): BaseCGIHandler.__init__(self, request.inp, request.out, request.err, request.env, \ multithread=self.wsgi_multithread, \ multiprocess=self.wsgi_multiprocess) # error handling. this is sent to the client by wsgiref.basehandler. def error_output(self, environ, start_response): ''' show traceback if we are so entitled. ''' start_response(self.error_status, self.error_headers[:], sys.exc_info()) if self.environ.get('REMOTE_ADDR') in SHOW_TRACEBACK_IPS: err = traceback.format_exc() else: err = self.error_body return ['wsgi app execution error\n', '-' * 24, '\n', err] def run(self, application): ''' Invoke the application ''' self.setup_environ() self.result = application(self.environ, self.start_response) self.finish_response() def serve_wsgi(app, handlerClass=FCGIHandler): ''' run (wsgi) app on each incoming FCGI request. ''' while True: req = FCGI() h = handlerClass(req) h.run(app) req.finish() del req #------------------------------------------------ # test the thing... def list_environment(environ, start_response): ''' Simple WSGI test application - just display the wsgi environment. Kinda useful, too for debugging. ''' status = '200 OK' response_headers = [('Content-type','text/plain')] start_response(status, response_headers) out = ['FcgiWsgiAdapter test output (just a listing of the WSGI environment)\n'] out.append('-' * (len(out[0]) -1) + '\n') for k,v in sorted(environ.items()): out.extend([k.ljust(25), ': ', str(v).strip(), '\n']) return out def test(): ''' to run this, import and run in an actual cgi or fcgi script ''' serve_wsgi(list_environment)