# Copyright (c) 2006 Allan Saddi # 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$ # # Copyright (c) 2011 Vladimir Rusinov __author__ = 'Allan Saddi ' __version__ = '$Revision$' import sys import select import struct import socket import errno import types __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 = '/www/server/mdserver-web/logs/fastcgi.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).encode() else: s = struct.pack('!L', nameLength | 0x80000000) valueLength = len(value) if valueLength < 128: s += chr(valueLength).encode() else: s += struct.pack('!L', valueLength | 0x80000000) 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 as 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 b''.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 as 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, b'\x00' * self.paddingLength) class FCGIApp(object): def __init__(self, connect=None, host=None, port=None, filterEnviron=True): if host is not None: assert port is not None connect = (host, port) self._connect = connect self._filterEnviron = filterEnviron def __call__(self, environ, io, start_response=None): # 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? # return '200 OK',[],str(params),'' self._fcgiParams(sock, requestId, params) self._fcgiParams(sock, requestId, {}) # Transfer wsgi.input to FCGI_STDIN content_length = int(environ.get('CONTENT_LENGTH') or 0) s = '' #io = StringIO(stdin) while True: if not io: break chunk_size = min(content_length, 4096) s = io.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) return sock def _getConnection(self): if self._connect is not None: # The simple case. Create a socket and connect to the # application. if isinstance(self._connect, str): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(self._connect) elif hasattr(socket, 'create_connection'): sock = socket.create_connection(self._connect) else: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(self._connect) return sock # To be done when I have more time... # , 'Launching and managing FastCGI programs not yet implemented' raise NotImplementedError 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.encode( 'latin-1'), value.encode('latin-1'))) data = b''.join(data) rec.contentData = data rec.contentLength = len(data) rec.write(sock) _environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_', 'CONTENT_', 'DOCUMENT_', 'SCRIPT_'] _environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE'] _environRenames = [] def _defaultFilterEnviron(self, environ): result = {} for n in environ.keys(): iv = False for p in self._environPrefixes: if n.startswith(p): result[n] = environ[n] iv = True if n in self._environCopies: result[n] = environ[n] iv = True if n in self._environRenames: result[self._environRenames[n]] = environ[n] iv = True if not iv: result[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