|
|
|
# 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$
|
|
|
|
#
|
|
|
|
# Copyright (c) 2011 Vladimir Rusinov <vladimir@greenmice.info>
|
|
|
|
|
|
|
|
__author__ = 'Allan Saddi <allan@saddi.com>'
|
|
|
|
__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
|