Back to index

moin  1.9.0~rc2
fcgi_app.py
Go to the documentation of this file.
00001 # Copyright (c) 2006 Allan Saddi <allan@saddi.com>
00002 # All rights reserved.
00003 #
00004 # Redistribution and use in source and binary forms, with or without
00005 # modification, are permitted provided that the following conditions
00006 # are met:
00007 # 1. Redistributions of source code must retain the above copyright
00008 #    notice, this list of conditions and the following disclaimer.
00009 # 2. Redistributions in binary form must reproduce the above copyright
00010 #    notice, this list of conditions and the following disclaimer in the
00011 #    documentation and/or other materials provided with the distribution.
00012 #
00013 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
00014 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
00015 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
00016 # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
00017 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
00018 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
00019 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
00020 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00021 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
00022 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
00023 # SUCH DAMAGE.
00024 #
00025 # $Id$
00026 
00027 __author__ = 'Allan Saddi <allan@saddi.com>'
00028 __version__ = '$Revision$'
00029 
00030 import select
00031 import struct
00032 import socket
00033 import errno
00034 
00035 __all__ = ['FCGIApp']
00036 
00037 # Constants from the spec.
00038 FCGI_LISTENSOCK_FILENO = 0
00039 
00040 FCGI_HEADER_LEN = 8
00041 
00042 FCGI_VERSION_1 = 1
00043 
00044 FCGI_BEGIN_REQUEST = 1
00045 FCGI_ABORT_REQUEST = 2
00046 FCGI_END_REQUEST = 3
00047 FCGI_PARAMS = 4
00048 FCGI_STDIN = 5
00049 FCGI_STDOUT = 6
00050 FCGI_STDERR = 7
00051 FCGI_DATA = 8
00052 FCGI_GET_VALUES = 9
00053 FCGI_GET_VALUES_RESULT = 10
00054 FCGI_UNKNOWN_TYPE = 11
00055 FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
00056 
00057 FCGI_NULL_REQUEST_ID = 0
00058 
00059 FCGI_KEEP_CONN = 1
00060 
00061 FCGI_RESPONDER = 1
00062 FCGI_AUTHORIZER = 2
00063 FCGI_FILTER = 3
00064 
00065 FCGI_REQUEST_COMPLETE = 0
00066 FCGI_CANT_MPX_CONN = 1
00067 FCGI_OVERLOADED = 2
00068 FCGI_UNKNOWN_ROLE = 3
00069 
00070 FCGI_MAX_CONNS = 'FCGI_MAX_CONNS'
00071 FCGI_MAX_REQS = 'FCGI_MAX_REQS'
00072 FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS'
00073 
00074 FCGI_Header = '!BBHHBx'
00075 FCGI_BeginRequestBody = '!HB5x'
00076 FCGI_EndRequestBody = '!LB3x'
00077 FCGI_UnknownTypeBody = '!B7x'
00078 
00079 FCGI_BeginRequestBody_LEN = struct.calcsize(FCGI_BeginRequestBody)
00080 FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody)
00081 FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody)
00082 
00083 if __debug__:
00084     import time
00085 
00086     # Set non-zero to write debug output to a file.
00087     DEBUG = 0
00088     DEBUGLOG = '/tmp/fcgi_app.log'
00089 
00090     def _debug(level, msg):
00091         if DEBUG < level:
00092             return
00093 
00094         try:
00095             f = open(DEBUGLOG, 'a')
00096             f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg))
00097             f.close()
00098         except:
00099             pass
00100 
00101 def decode_pair(s, pos=0):
00102     """
00103     Decodes a name/value pair.
00104 
00105     The number of bytes decoded as well as the name/value pair
00106     are returned.
00107     """
00108     nameLength = ord(s[pos])
00109     if nameLength & 128:
00110         nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
00111         pos += 4
00112     else:
00113         pos += 1
00114 
00115     valueLength = ord(s[pos])
00116     if valueLength & 128:
00117         valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
00118         pos += 4
00119     else:
00120         pos += 1
00121 
00122     name = s[pos:pos+nameLength]
00123     pos += nameLength
00124     value = s[pos:pos+valueLength]
00125     pos += valueLength
00126 
00127     return (pos, (name, value))
00128 
00129 def encode_pair(name, value):
00130     """
00131     Encodes a name/value pair.
00132 
00133     The encoded string is returned.
00134     """
00135     nameLength = len(name)
00136     if nameLength < 128:
00137         s = chr(nameLength)
00138     else:
00139         s = struct.pack('!L', nameLength | 0x80000000L)
00140 
00141     valueLength = len(value)
00142     if valueLength < 128:
00143         s += chr(valueLength)
00144     else:
00145         s += struct.pack('!L', valueLength | 0x80000000L)
00146 
00147     return s + name + value
00148 
00149 class Record(object):
00150     """
00151     A FastCGI Record.
00152 
00153     Used for encoding/decoding records.
00154     """
00155     def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID):
00156         self.version = FCGI_VERSION_1
00157         self.type = type
00158         self.requestId = requestId
00159         self.contentLength = 0
00160         self.paddingLength = 0
00161         self.contentData = ''
00162 
00163     def _recvall(sock, length):
00164         """
00165         Attempts to receive length bytes from a socket, blocking if necessary.
00166         (Socket may be blocking or non-blocking.)
00167         """
00168         dataList = []
00169         recvLen = 0
00170         while length:
00171             try:
00172                 data = sock.recv(length)
00173             except socket.error, e:
00174                 if e[0] == errno.EAGAIN:
00175                     select.select([sock], [], [])
00176                     continue
00177                 else:
00178                     raise
00179             if not data: # EOF
00180                 break
00181             dataList.append(data)
00182             dataLen = len(data)
00183             recvLen += dataLen
00184             length -= dataLen
00185         return ''.join(dataList), recvLen
00186     _recvall = staticmethod(_recvall)
00187 
00188     def read(self, sock):
00189         """Read and decode a Record from a socket."""
00190         try:
00191             header, length = self._recvall(sock, FCGI_HEADER_LEN)
00192         except:
00193             raise EOFError
00194 
00195         if length < FCGI_HEADER_LEN:
00196             raise EOFError
00197         
00198         self.version, self.type, self.requestId, self.contentLength, \
00199                       self.paddingLength = struct.unpack(FCGI_Header, header)
00200 
00201         if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, '
00202                              'contentLength = %d' %
00203                              (sock.fileno(), self.type, self.requestId,
00204                               self.contentLength))
00205         
00206         if self.contentLength:
00207             try:
00208                 self.contentData, length = self._recvall(sock,
00209                                                          self.contentLength)
00210             except:
00211                 raise EOFError
00212 
00213             if length < self.contentLength:
00214                 raise EOFError
00215 
00216         if self.paddingLength:
00217             try:
00218                 self._recvall(sock, self.paddingLength)
00219             except:
00220                 raise EOFError
00221 
00222     def _sendall(sock, data):
00223         """
00224         Writes data to a socket and does not return until all the data is sent.
00225         """
00226         length = len(data)
00227         while length:
00228             try:
00229                 sent = sock.send(data)
00230             except socket.error, e:
00231                 if e[0] == errno.EAGAIN:
00232                     select.select([], [sock], [])
00233                     continue
00234                 else:
00235                     raise
00236             data = data[sent:]
00237             length -= sent
00238     _sendall = staticmethod(_sendall)
00239 
00240     def write(self, sock):
00241         """Encode and write a Record to a socket."""
00242         self.paddingLength = -self.contentLength & 7
00243 
00244         if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, '
00245                              'contentLength = %d' %
00246                              (sock.fileno(), self.type, self.requestId,
00247                               self.contentLength))
00248 
00249         header = struct.pack(FCGI_Header, self.version, self.type,
00250                              self.requestId, self.contentLength,
00251                              self.paddingLength)
00252         self._sendall(sock, header)
00253         if self.contentLength:
00254             self._sendall(sock, self.contentData)
00255         if self.paddingLength:
00256             self._sendall(sock, '\x00'*self.paddingLength)
00257 
00258 class FCGIApp(object):
00259     def __init__(self, command=None, connect=None, host=None, port=None,
00260                  filterEnviron=True):
00261         if host is not None:
00262             assert port is not None
00263             connect=(host, port)
00264 
00265         assert (command is not None and connect is None) or \
00266                (command is None and connect is not None)
00267 
00268         self._command = command
00269         self._connect = connect
00270 
00271         self._filterEnviron = filterEnviron
00272         
00273         #sock = self._getConnection()
00274         #print self._fcgiGetValues(sock, ['FCGI_MAX_CONNS', 'FCGI_MAX_REQS', 'FCGI_MPXS_CONNS'])
00275         #sock.close()
00276         
00277     def __call__(self, environ, start_response):
00278         # For sanity's sake, we don't care about FCGI_MPXS_CONN
00279         # (connection multiplexing). For every request, we obtain a new
00280         # transport socket, perform the request, then discard the socket.
00281         # This is, I believe, how mod_fastcgi does things...
00282 
00283         sock = self._getConnection()
00284 
00285         # Since this is going to be the only request on this connection,
00286         # set the request ID to 1.
00287         requestId = 1
00288 
00289         # Begin the request
00290         rec = Record(FCGI_BEGIN_REQUEST, requestId)
00291         rec.contentData = struct.pack(FCGI_BeginRequestBody, FCGI_RESPONDER, 0)
00292         rec.contentLength = FCGI_BeginRequestBody_LEN
00293         rec.write(sock)
00294 
00295         # Filter WSGI environ and send it as FCGI_PARAMS
00296         if self._filterEnviron:
00297             params = self._defaultFilterEnviron(environ)
00298         else:
00299             params = self._lightFilterEnviron(environ)
00300         # TODO: Anything not from environ that needs to be sent also?
00301         self._fcgiParams(sock, requestId, params)
00302         self._fcgiParams(sock, requestId, {})
00303 
00304         # Transfer wsgi.input to FCGI_STDIN
00305         content_length = int(environ.get('CONTENT_LENGTH') or 0)
00306         while True:
00307             chunk_size = min(content_length, 4096)
00308             s = environ['wsgi.input'].read(chunk_size)
00309             content_length -= len(s)
00310             rec = Record(FCGI_STDIN, requestId)
00311             rec.contentData = s
00312             rec.contentLength = len(s)
00313             rec.write(sock)
00314 
00315             if not s: break
00316 
00317         # Empty FCGI_DATA stream
00318         rec = Record(FCGI_DATA, requestId)
00319         rec.write(sock)
00320 
00321         # Main loop. Process FCGI_STDOUT, FCGI_STDERR, FCGI_END_REQUEST
00322         # records from the application.
00323         result = []
00324         while True:
00325             inrec = Record()
00326             inrec.read(sock)
00327             if inrec.type == FCGI_STDOUT:
00328                 if inrec.contentData:
00329                     result.append(inrec.contentData)
00330                 else:
00331                     # TODO: Should probably be pedantic and no longer
00332                     # accept FCGI_STDOUT records?
00333                     pass
00334             elif inrec.type == FCGI_STDERR:
00335                 # Simply forward to wsgi.errors
00336                 environ['wsgi.errors'].write(inrec.contentData)
00337             elif inrec.type == FCGI_END_REQUEST:
00338                 # TODO: Process appStatus/protocolStatus fields?
00339                 break
00340 
00341         # Done with this transport socket, close it. (FCGI_KEEP_CONN was not
00342         # set in the FCGI_BEGIN_REQUEST record we sent above. So the
00343         # application is expected to do the same.)
00344         sock.close()
00345 
00346         result = ''.join(result)
00347 
00348         # Parse response headers from FCGI_STDOUT
00349         status = '200 OK'
00350         headers = []
00351         pos = 0
00352         while True:
00353             eolpos = result.find('\n', pos)
00354             if eolpos < 0: break
00355             line = result[pos:eolpos-1]
00356             pos = eolpos + 1
00357 
00358             # strip in case of CR. NB: This will also strip other
00359             # whitespace...
00360             line = line.strip()
00361             
00362             # Empty line signifies end of headers
00363             if not line: break
00364 
00365             # TODO: Better error handling
00366             header, value = line.split(':', 1)
00367             header = header.strip().lower()
00368             value = value.strip()
00369 
00370             if header == 'status':
00371                 # Special handling of Status header
00372                 status = value
00373                 if status.find(' ') < 0:
00374                     # Append a dummy reason phrase if one was not provided
00375                     status += ' FCGIApp'
00376             else:
00377                 headers.append((header, value))
00378 
00379         result = result[pos:]
00380 
00381         # Set WSGI status, headers, and return result.
00382         start_response(status, headers)
00383         return [result]
00384 
00385     def _getConnection(self):
00386         if self._connect is not None:
00387             # The simple case. Create a socket and connect to the
00388             # application.
00389             if type(self._connect) is str:
00390                 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
00391             else:
00392                 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
00393             sock.connect(self._connect)
00394             return sock
00395 
00396         # To be done when I have more time...
00397         raise NotImplementedError, 'Launching and managing FastCGI programs not yet implemented'
00398     
00399     def _fcgiGetValues(self, sock, vars):
00400         # Construct FCGI_GET_VALUES record
00401         outrec = Record(FCGI_GET_VALUES)
00402         data = []
00403         for name in vars:
00404             data.append(encode_pair(name, ''))
00405         data = ''.join(data)
00406         outrec.contentData = data
00407         outrec.contentLength = len(data)
00408         outrec.write(sock)
00409 
00410         # Await response
00411         inrec = Record()
00412         inrec.read(sock)
00413         result = {}
00414         if inrec.type == FCGI_GET_VALUES_RESULT:
00415             pos = 0
00416             while pos < inrec.contentLength:
00417                 pos, (name, value) = decode_pair(inrec.contentData, pos)
00418                 result[name] = value
00419         return result
00420 
00421     def _fcgiParams(self, sock, requestId, params):
00422         rec = Record(FCGI_PARAMS, requestId)
00423         data = []
00424         for name,value in params.items():
00425             data.append(encode_pair(name, value))
00426         data = ''.join(data)
00427         rec.contentData = data
00428         rec.contentLength = len(data)
00429         rec.write(sock)
00430 
00431     _environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_',
00432                         'CONTENT_']
00433     _environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE']
00434     _environRenames = {}
00435 
00436     def _defaultFilterEnviron(self, environ):
00437         result = {}
00438         for n in environ.keys():
00439             for p in self._environPrefixes:
00440                 if n.startswith(p):
00441                     result[n] = environ[n]
00442             if n in self._environCopies:
00443                 result[n] = environ[n]
00444             if n in self._environRenames:
00445                 result[self._environRenames[n]] = environ[n]
00446                 
00447         return result
00448 
00449     def _lightFilterEnviron(self, environ):
00450         result = {}
00451         for n in environ.keys():
00452             if n.upper() == n:
00453                 result[n] = environ[n]
00454         return result
00455 
00456 if __name__ == '__main__':
00457     from flup.server.ajp import WSGIServer
00458     app = FCGIApp(connect=('localhost', 4242))
00459     #import paste.lint
00460     #app = paste.lint.middleware(app)
00461     WSGIServer(app).run()