mirror of https://github.com/midoks/mdserver-web
parent
f91be53005
commit
60fe47be21
@ -0,0 +1,461 @@ |
||||
# 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 | 0x80000000) |
||||
|
||||
valueLength = len(value) |
||||
if valueLength < 128: |
||||
s += chr(valueLength) |
||||
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.errno == 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 as e: |
||||
if e.errno == 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() |
Loading…
Reference in new issue