123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- # Copyright (c) 2006 Allan Saddi <allan@saddi.com>
- # 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.
- #
- # $Id$
- __author__ = 'Allan Saddi <allan@saddi.com>'
- __version__ = '$Revision$'
- import select
- import struct
- import socket
- import errno
- __all__ = ['FCGIApp']
- # Constants from the spec.
- FCGI_LISTENSOCK_FILENO = 0
- FCGI_HEADER_LEN = 8
- FCGI_VERSION_1 = 1
- 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
- FCGI_NULL_REQUEST_ID = 0
- FCGI_KEEP_CONN = 1
- FCGI_RESPONDER = 1
- FCGI_AUTHORIZER = 2
- FCGI_FILTER = 3
- FCGI_REQUEST_COMPLETE = 0
- FCGI_CANT_MPX_CONN = 1
- FCGI_OVERLOADED = 2
- FCGI_UNKNOWN_ROLE = 3
- FCGI_MAX_CONNS = 'FCGI_MAX_CONNS'
- FCGI_MAX_REQS = 'FCGI_MAX_REQS'
- FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS'
- FCGI_Header = '!BBHHBx'
- FCGI_BeginRequestBody = '!HB5x'
- FCGI_EndRequestBody = '!LB3x'
- FCGI_UnknownTypeBody = '!B7x'
- FCGI_BeginRequestBody_LEN = struct.calcsize(FCGI_BeginRequestBody)
- FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody)
- FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody)
- if __debug__:
- import time
- # Set non-zero to write debug output to a file.
- DEBUG = 0
- DEBUGLOG = '/tmp/fcgi_app.log'
- def _debug(level, msg):
- if DEBUG < level:
- return
- try:
- f = open(DEBUGLOG, 'a')
- f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg))
- f.close()
- except:
- pass
- def decode_pair(s, pos=0):
- """
- Decodes a name/value pair.
- The number of bytes decoded as well as the name/value pair
- are returned.
- """
- nameLength = ord(s[pos])
- if nameLength & 128:
- nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
- pos += 4
- else:
- pos += 1
- valueLength = ord(s[pos])
- if valueLength & 128:
- valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
- pos += 4
- else:
- pos += 1
- name = s[pos:pos+nameLength]
- pos += nameLength
- value = s[pos:pos+valueLength]
- pos += valueLength
- return (pos, (name, value))
- def encode_pair(name, value):
- """
- Encodes a name/value pair.
- The encoded string is returned.
- """
- nameLength = len(name)
- if nameLength < 128:
- s = chr(nameLength)
- else:
- s = struct.pack('!L', nameLength | 0x80000000L)
- valueLength = len(value)
- if valueLength < 128:
- s += chr(valueLength)
- else:
- s += struct.pack('!L', valueLength | 0x80000000L)
- return s + name + value
- class Record(object):
- """
- A FastCGI Record.
- Used for encoding/decoding records.
- """
- def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID):
- self.version = FCGI_VERSION_1
- self.type = type
- self.requestId = requestId
- self.contentLength = 0
- self.paddingLength = 0
- self.contentData = ''
- def _recvall(sock, length):
- """
- Attempts to receive length bytes from a socket, blocking if necessary.
- (Socket may be blocking or non-blocking.)
- """
- dataList = []
- recvLen = 0
- while length:
- try:
- data = sock.recv(length)
- except socket.error, e:
- if e[0] == errno.EAGAIN:
- select.select([sock], [], [])
- continue
- else:
- raise
- if not data: # EOF
- break
- dataList.append(data)
- dataLen = len(data)
- recvLen += dataLen
- length -= dataLen
- return ''.join(dataList), recvLen
- _recvall = staticmethod(_recvall)
- def read(self, sock):
- """Read and decode a Record from a socket."""
- try:
- header, length = self._recvall(sock, FCGI_HEADER_LEN)
- except:
- raise EOFError
- if length < FCGI_HEADER_LEN:
- raise EOFError
-
- self.version, self.type, self.requestId, self.contentLength, \
- self.paddingLength = struct.unpack(FCGI_Header, header)
- if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, '
- 'contentLength = %d' %
- (sock.fileno(), self.type, self.requestId,
- self.contentLength))
-
- if self.contentLength:
- try:
- self.contentData, length = self._recvall(sock,
- self.contentLength)
- except:
- raise EOFError
- if length < self.contentLength:
- raise EOFError
- if self.paddingLength:
- try:
- self._recvall(sock, self.paddingLength)
- except:
- raise EOFError
- def _sendall(sock, data):
- """
- Writes data to a socket and does not return until all the data is sent.
- """
- length = len(data)
- while length:
- try:
- sent = sock.send(data)
- except socket.error, e:
- if e[0] == errno.EAGAIN:
- select.select([], [sock], [])
- continue
- else:
- raise
- data = data[sent:]
- length -= sent
- _sendall = staticmethod(_sendall)
- def write(self, sock):
- """Encode and write a Record to a socket."""
- self.paddingLength = -self.contentLength & 7
- if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, '
- 'contentLength = %d' %
- (sock.fileno(), self.type, self.requestId,
- self.contentLength))
- header = struct.pack(FCGI_Header, self.version, self.type,
- self.requestId, self.contentLength,
- self.paddingLength)
- self._sendall(sock, header)
- if self.contentLength:
- self._sendall(sock, self.contentData)
- if self.paddingLength:
- self._sendall(sock, '\x00'*self.paddingLength)
- class FCGIApp(object):
- def __init__(self, command=None, connect=None, host=None, port=None,
- filterEnviron=True):
- if host is not None:
- assert port is not None
- connect=(host, port)
- assert (command is not None and connect is None) or \
- (command is None and connect is not None)
- self._command = command
- self._connect = connect
- self._filterEnviron = filterEnviron
-
- #sock = self._getConnection()
- #print self._fcgiGetValues(sock, ['FCGI_MAX_CONNS', 'FCGI_MAX_REQS', 'FCGI_MPXS_CONNS'])
- #sock.close()
-
- def __call__(self, environ, start_response):
- # For sanity's sake, we don't care about FCGI_MPXS_CONN
- # (connection multiplexing). For every request, we obtain a new
- # transport socket, perform the request, then discard the socket.
- # This is, I believe, how mod_fastcgi does things...
- sock = self._getConnection()
- # Since this is going to be the only request on this connection,
- # set the request ID to 1.
- requestId = 1
- # Begin the request
- rec = Record(FCGI_BEGIN_REQUEST, requestId)
- rec.contentData = struct.pack(FCGI_BeginRequestBody, FCGI_RESPONDER, 0)
- rec.contentLength = FCGI_BeginRequestBody_LEN
- rec.write(sock)
- # Filter WSGI environ and send it as FCGI_PARAMS
- if self._filterEnviron:
- params = self._defaultFilterEnviron(environ)
- else:
- params = self._lightFilterEnviron(environ)
- # TODO: Anything not from environ that needs to be sent also?
- self._fcgiParams(sock, requestId, params)
- self._fcgiParams(sock, requestId, {})
- # Transfer wsgi.input to FCGI_STDIN
- content_length = int(environ.get('CONTENT_LENGTH') or 0)
- while True:
- chunk_size = min(content_length, 4096)
- s = environ['wsgi.input'].read(chunk_size)
- content_length -= len(s)
- rec = Record(FCGI_STDIN, requestId)
- rec.contentData = s
- rec.contentLength = len(s)
- rec.write(sock)
- if not s: break
- # Empty FCGI_DATA stream
- rec = Record(FCGI_DATA, requestId)
- rec.write(sock)
- # Main loop. Process FCGI_STDOUT, FCGI_STDERR, FCGI_END_REQUEST
- # records from the application.
- result = []
- while True:
- inrec = Record()
- inrec.read(sock)
- if inrec.type == FCGI_STDOUT:
- if inrec.contentData:
- result.append(inrec.contentData)
- else:
- # TODO: Should probably be pedantic and no longer
- # accept FCGI_STDOUT records?
- pass
- elif inrec.type == FCGI_STDERR:
- # Simply forward to wsgi.errors
- environ['wsgi.errors'].write(inrec.contentData)
- elif inrec.type == FCGI_END_REQUEST:
- # TODO: Process appStatus/protocolStatus fields?
- break
- # Done with this transport socket, close it. (FCGI_KEEP_CONN was not
- # set in the FCGI_BEGIN_REQUEST record we sent above. So the
- # application is expected to do the same.)
- sock.close()
- result = ''.join(result)
- # Parse response headers from FCGI_STDOUT
- status = '200 OK'
- headers = []
- pos = 0
- while True:
- eolpos = result.find('\n', pos)
- if eolpos < 0: break
- line = result[pos:eolpos-1]
- pos = eolpos + 1
- # strip in case of CR. NB: This will also strip other
- # whitespace...
- line = line.strip()
-
- # Empty line signifies end of headers
- if not line: break
- # TODO: Better error handling
- header, value = line.split(':', 1)
- header = header.strip().lower()
- value = value.strip()
- if header == 'status':
- # Special handling of Status header
- status = value
- if status.find(' ') < 0:
- # Append a dummy reason phrase if one was not provided
- status += ' FCGIApp'
- else:
- headers.append((header, value))
- result = result[pos:]
- # Set WSGI status, headers, and return result.
- start_response(status, headers)
- return [result]
- def _getConnection(self):
- if self._connect is not None:
- # The simple case. Create a socket and connect to the
- # application.
- if type(self._connect) is str:
- sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- else:
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.connect(self._connect)
- return sock
- # To be done when I have more time...
- raise NotImplementedError, 'Launching and managing FastCGI programs not yet implemented'
-
- def _fcgiGetValues(self, sock, vars):
- # Construct FCGI_GET_VALUES record
- outrec = Record(FCGI_GET_VALUES)
- data = []
- for name in vars:
- data.append(encode_pair(name, ''))
- data = ''.join(data)
- outrec.contentData = data
- outrec.contentLength = len(data)
- outrec.write(sock)
- # Await response
- inrec = Record()
- inrec.read(sock)
- result = {}
- if inrec.type == FCGI_GET_VALUES_RESULT:
- pos = 0
- while pos < inrec.contentLength:
- pos, (name, value) = decode_pair(inrec.contentData, pos)
- result[name] = value
- return result
- def _fcgiParams(self, sock, requestId, params):
- rec = Record(FCGI_PARAMS, requestId)
- data = []
- for name,value in params.items():
- data.append(encode_pair(name, value))
- data = ''.join(data)
- rec.contentData = data
- rec.contentLength = len(data)
- rec.write(sock)
- _environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_',
- 'CONTENT_']
- _environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE']
- _environRenames = {}
- def _defaultFilterEnviron(self, environ):
- result = {}
- for n in environ.keys():
- for p in self._environPrefixes:
- if n.startswith(p):
- result[n] = environ[n]
- if n in self._environCopies:
- result[n] = environ[n]
- if n in self._environRenames:
- result[self._environRenames[n]] = environ[n]
-
- return result
- def _lightFilterEnviron(self, environ):
- result = {}
- for n in environ.keys():
- if n.upper() == n:
- result[n] = environ[n]
- return result
- if __name__ == '__main__':
- from flup.server.ajp import WSGIServer
- app = FCGIApp(connect=('localhost', 4242))
- #import paste.lint
- #app = paste.lint.middleware(app)
- WSGIServer(app).run()
|