1
# Copyright (c) 2006 Allan Saddi <allan@saddi.com>
4
# Redistribution and use in source and binary forms, with or without
5
# modification, are permitted provided that the following conditions
7
# 1. Redistributions of source code must retain the above copyright
8
# notice, this list of conditions and the following disclaimer.
9
# 2. Redistributions in binary form must reproduce the above copyright
10
# notice, this list of conditions and the following disclaimer in the
11
# documentation and/or other materials provided with the distribution.
13
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27
__author__ = 'Allan Saddi <allan@saddi.com>'
28
__version__ = '$Revision$'
37
# Constants from the spec.
38
FCGI_LISTENSOCK_FILENO = 0
44
FCGI_BEGIN_REQUEST = 1
45
FCGI_ABORT_REQUEST = 2
53
FCGI_GET_VALUES_RESULT = 10
54
FCGI_UNKNOWN_TYPE = 11
55
FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
57
FCGI_NULL_REQUEST_ID = 0
65
FCGI_REQUEST_COMPLETE = 0
66
FCGI_CANT_MPX_CONN = 1
70
FCGI_MAX_CONNS = 'FCGI_MAX_CONNS'
71
FCGI_MAX_REQS = 'FCGI_MAX_REQS'
72
FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS'
74
FCGI_Header = '!BBHHBx'
75
FCGI_BeginRequestBody = '!HB5x'
76
FCGI_EndRequestBody = '!LB3x'
77
FCGI_UnknownTypeBody = '!B7x'
79
FCGI_BeginRequestBody_LEN = struct.calcsize(FCGI_BeginRequestBody)
80
FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody)
81
FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody)
86
# Set non-zero to write debug output to a file.
88
DEBUGLOG = '/tmp/fcgi_app.log'
90
def _debug(level, msg):
95
f = open(DEBUGLOG, 'a')
96
f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg))
101
def decode_pair(s, pos=0):
103
Decodes a name/value pair.
105
The number of bytes decoded as well as the name/value pair
108
nameLength = ord(s[pos])
110
nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
115
valueLength = ord(s[pos])
116
if valueLength & 128:
117
valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
122
name = s[pos:pos+nameLength]
124
value = s[pos:pos+valueLength]
127
return (pos, (name, value))
129
def encode_pair(name, value):
131
Encodes a name/value pair.
133
The encoded string is returned.
135
nameLength = len(name)
139
s = struct.pack('!L', nameLength | 0x80000000L)
141
valueLength = len(value)
142
if valueLength < 128:
143
s += chr(valueLength)
145
s += struct.pack('!L', valueLength | 0x80000000L)
147
return s + name + value
149
class Record(object):
153
Used for encoding/decoding records.
155
def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID):
156
self.version = FCGI_VERSION_1
158
self.requestId = requestId
159
self.contentLength = 0
160
self.paddingLength = 0
161
self.contentData = ''
163
def _recvall(sock, length):
165
Attempts to receive length bytes from a socket, blocking if necessary.
166
(Socket may be blocking or non-blocking.)
172
data = sock.recv(length)
173
except socket.error, e:
174
if e[0] == errno.EAGAIN:
175
select.select([sock], [], [])
181
dataList.append(data)
185
return ''.join(dataList), recvLen
186
_recvall = staticmethod(_recvall)
188
def read(self, sock):
189
"""Read and decode a Record from a socket."""
191
header, length = self._recvall(sock, FCGI_HEADER_LEN)
195
if length < FCGI_HEADER_LEN:
198
self.version, self.type, self.requestId, self.contentLength, \
199
self.paddingLength = struct.unpack(FCGI_Header, header)
201
if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, '
202
'contentLength = %d' %
203
(sock.fileno(), self.type, self.requestId,
206
if self.contentLength:
208
self.contentData, length = self._recvall(sock,
213
if length < self.contentLength:
216
if self.paddingLength:
218
self._recvall(sock, self.paddingLength)
222
def _sendall(sock, data):
224
Writes data to a socket and does not return until all the data is sent.
229
sent = sock.send(data)
230
except socket.error, e:
231
if e[0] == errno.EAGAIN:
232
select.select([], [sock], [])
238
_sendall = staticmethod(_sendall)
240
def write(self, sock):
241
"""Encode and write a Record to a socket."""
242
self.paddingLength = -self.contentLength & 7
244
if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, '
245
'contentLength = %d' %
246
(sock.fileno(), self.type, self.requestId,
249
header = struct.pack(FCGI_Header, self.version, self.type,
250
self.requestId, self.contentLength,
252
self._sendall(sock, header)
253
if self.contentLength:
254
self._sendall(sock, self.contentData)
255
if self.paddingLength:
256
self._sendall(sock, '\x00'*self.paddingLength)
258
class FCGIApp(object):
259
def __init__(self, command=None, connect=None, host=None, port=None,
262
assert port is not None
265
assert (command is not None and connect is None) or \
266
(command is None and connect is not None)
268
self._command = command
269
self._connect = connect
271
self._filterEnviron = filterEnviron
273
#sock = self._getConnection()
274
#print self._fcgiGetValues(sock, ['FCGI_MAX_CONNS', 'FCGI_MAX_REQS', 'FCGI_MPXS_CONNS'])
277
def __call__(self, environ, start_response):
278
# For sanity's sake, we don't care about FCGI_MPXS_CONN
279
# (connection multiplexing). For every request, we obtain a new
280
# transport socket, perform the request, then discard the socket.
281
# This is, I believe, how mod_fastcgi does things...
283
sock = self._getConnection()
285
# Since this is going to be the only request on this connection,
286
# set the request ID to 1.
290
rec = Record(FCGI_BEGIN_REQUEST, requestId)
291
rec.contentData = struct.pack(FCGI_BeginRequestBody, FCGI_RESPONDER, 0)
292
rec.contentLength = FCGI_BeginRequestBody_LEN
295
# Filter WSGI environ and send it as FCGI_PARAMS
296
if self._filterEnviron:
297
params = self._defaultFilterEnviron(environ)
299
params = self._lightFilterEnviron(environ)
300
# TODO: Anything not from environ that needs to be sent also?
301
self._fcgiParams(sock, requestId, params)
302
self._fcgiParams(sock, requestId, {})
304
# Transfer wsgi.input to FCGI_STDIN
305
content_length = int(environ.get('CONTENT_LENGTH') or 0)
307
chunk_size = min(content_length, 4096)
308
s = environ['wsgi.input'].read(chunk_size)
309
content_length -= len(s)
310
rec = Record(FCGI_STDIN, requestId)
312
rec.contentLength = len(s)
317
# Empty FCGI_DATA stream
318
rec = Record(FCGI_DATA, requestId)
321
# Main loop. Process FCGI_STDOUT, FCGI_STDERR, FCGI_END_REQUEST
322
# records from the application.
327
if inrec.type == FCGI_STDOUT:
328
if inrec.contentData:
329
result.append(inrec.contentData)
331
# TODO: Should probably be pedantic and no longer
332
# accept FCGI_STDOUT records?
334
elif inrec.type == FCGI_STDERR:
335
# Simply forward to wsgi.errors
336
environ['wsgi.errors'].write(inrec.contentData)
337
elif inrec.type == FCGI_END_REQUEST:
338
# TODO: Process appStatus/protocolStatus fields?
341
# Done with this transport socket, close it. (FCGI_KEEP_CONN was not
342
# set in the FCGI_BEGIN_REQUEST record we sent above. So the
343
# application is expected to do the same.)
346
result = ''.join(result)
348
# Parse response headers from FCGI_STDOUT
353
eolpos = result.find('\n', pos)
355
line = result[pos:eolpos-1]
358
# strip in case of CR. NB: This will also strip other
362
# Empty line signifies end of headers
365
# TODO: Better error handling
366
header, value = line.split(':', 1)
367
header = header.strip().lower()
368
value = value.strip()
370
if header == 'status':
371
# Special handling of Status header
373
if status.find(' ') < 0:
374
# Append a dummy reason phrase if one was not provided
377
headers.append((header, value))
379
result = result[pos:]
381
# Set WSGI status, headers, and return result.
382
start_response(status, headers)
385
def _getConnection(self):
386
if self._connect is not None:
387
# The simple case. Create a socket and connect to the
389
if type(self._connect) is str:
390
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
392
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
393
sock.connect(self._connect)
396
# To be done when I have more time...
397
raise NotImplementedError, 'Launching and managing FastCGI programs not yet implemented'
399
def _fcgiGetValues(self, sock, vars):
400
# Construct FCGI_GET_VALUES record
401
outrec = Record(FCGI_GET_VALUES)
404
data.append(encode_pair(name, ''))
406
outrec.contentData = data
407
outrec.contentLength = len(data)
414
if inrec.type == FCGI_GET_VALUES_RESULT:
416
while pos < inrec.contentLength:
417
pos, (name, value) = decode_pair(inrec.contentData, pos)
421
def _fcgiParams(self, sock, requestId, params):
422
rec = Record(FCGI_PARAMS, requestId)
424
for name,value in params.items():
425
data.append(encode_pair(name, value))
427
rec.contentData = data
428
rec.contentLength = len(data)
431
_environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_',
433
_environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE']
436
def _defaultFilterEnviron(self, environ):
438
for n in environ.keys():
439
for p in self._environPrefixes:
441
result[n] = environ[n]
442
if n in self._environCopies:
443
result[n] = environ[n]
444
if n in self._environRenames:
445
result[self._environRenames[n]] = environ[n]
449
def _lightFilterEnviron(self, environ):
451
for n in environ.keys():
453
result[n] = environ[n]
456
if __name__ == '__main__':
457
from flup.server.ajp import WSGIServer
458
app = FCGIApp(connect=('localhost', 4242))
460
#app = paste.lint.middleware(app)
461
WSGIServer(app).run()