1# Copyright (c) 2006 Allan Saddi <allan@saddi.com> 2# All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions 6# are met: 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. 12# 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 23# SUCH DAMAGE. 24# 25# $Id$ 26 27__author__ = 'Allan Saddi <allan@saddi.com>' 28__version__ = '$Revision$' 29 30import select 31import struct 32import socket 33import errno 34 35__all__ = ['FCGIApp'] 36 37# Constants from the spec. 38FCGI_LISTENSOCK_FILENO = 0 39 40FCGI_HEADER_LEN = 8 41 42FCGI_VERSION_1 = 1 43 44FCGI_BEGIN_REQUEST = 1 45FCGI_ABORT_REQUEST = 2 46FCGI_END_REQUEST = 3 47FCGI_PARAMS = 4 48FCGI_STDIN = 5 49FCGI_STDOUT = 6 50FCGI_STDERR = 7 51FCGI_DATA = 8 52FCGI_GET_VALUES = 9 53FCGI_GET_VALUES_RESULT = 10 54FCGI_UNKNOWN_TYPE = 11 55FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE 56 57FCGI_NULL_REQUEST_ID = 0 58 59FCGI_KEEP_CONN = 1 60 61FCGI_RESPONDER = 1 62FCGI_AUTHORIZER = 2 63FCGI_FILTER = 3 64 65FCGI_REQUEST_COMPLETE = 0 66FCGI_CANT_MPX_CONN = 1 67FCGI_OVERLOADED = 2 68FCGI_UNKNOWN_ROLE = 3 69 70FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' 71FCGI_MAX_REQS = 'FCGI_MAX_REQS' 72FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' 73 74FCGI_Header = '!BBHHBx' 75FCGI_BeginRequestBody = '!HB5x' 76FCGI_EndRequestBody = '!LB3x' 77FCGI_UnknownTypeBody = '!B7x' 78 79FCGI_BeginRequestBody_LEN = struct.calcsize(FCGI_BeginRequestBody) 80FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody) 81FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody) 82 83if __debug__: 84 import time 85 86 # Set non-zero to write debug output to a file. 87 DEBUG = 0 88 DEBUGLOG = '/tmp/fcgi_app.log' 89 90 def _debug(level, msg): 91 if DEBUG < level: 92 return 93 94 try: 95 f = open(DEBUGLOG, 'a') 96 f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg)) 97 f.close() 98 except: 99 pass 100 101def decode_pair(s, pos=0): 102 """ 103 Decodes a name/value pair. 104 105 The number of bytes decoded as well as the name/value pair 106 are returned. 107 """ 108 nameLength = ord(s[pos]) 109 if nameLength & 128: 110 nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff 111 pos += 4 112 else: 113 pos += 1 114 115 valueLength = ord(s[pos]) 116 if valueLength & 128: 117 valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff 118 pos += 4 119 else: 120 pos += 1 121 122 name = s[pos:pos+nameLength] 123 pos += nameLength 124 value = s[pos:pos+valueLength] 125 pos += valueLength 126 127 return (pos, (name, value)) 128 129def encode_pair(name, value): 130 """ 131 Encodes a name/value pair. 132 133 The encoded string is returned. 134 """ 135 nameLength = len(name) 136 if nameLength < 128: 137 s = chr(nameLength) 138 else: 139 s = struct.pack('!L', nameLength | 0x80000000) 140 141 valueLength = len(value) 142 if valueLength < 128: 143 s += chr(valueLength) 144 else: 145 s += struct.pack('!L', valueLength | 0x80000000) 146 147 return s + name + value 148 149class Record(object): 150 """ 151 A FastCGI Record. 152 153 Used for encoding/decoding records. 154 """ 155 def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID): 156 self.version = FCGI_VERSION_1 157 self.type = type 158 self.requestId = requestId 159 self.contentLength = 0 160 self.paddingLength = 0 161 self.contentData = '' 162 163 def _recvall(sock, length): 164 """ 165 Attempts to receive length bytes from a socket, blocking if necessary. 166 (Socket may be blocking or non-blocking.) 167 """ 168 dataList = [] 169 recvLen = 0 170 while length: 171 try: 172 data = sock.recv(length) 173 except socket.error as e: 174 if e.errno == errno.EAGAIN: 175 select.select([sock], [], []) 176 continue 177 else: 178 raise 179 if not data: # EOF 180 break 181 dataList.append(data) 182 dataLen = len(data) 183 recvLen += dataLen 184 length -= dataLen 185 return ''.join(dataList), recvLen 186 _recvall = staticmethod(_recvall) 187 188 def read(self, sock): 189 """Read and decode a Record from a socket.""" 190 try: 191 header, length = self._recvall(sock, FCGI_HEADER_LEN) 192 except: 193 raise EOFError 194 195 if length < FCGI_HEADER_LEN: 196 raise EOFError 197 198 self.version, self.type, self.requestId, self.contentLength, \ 199 self.paddingLength = struct.unpack(FCGI_Header, header) 200 201 if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, ' 202 'contentLength = %d' % 203 (sock.fileno(), self.type, self.requestId, 204 self.contentLength)) 205 206 if self.contentLength: 207 try: 208 self.contentData, length = self._recvall(sock, 209 self.contentLength) 210 except: 211 raise EOFError 212 213 if length < self.contentLength: 214 raise EOFError 215 216 if self.paddingLength: 217 try: 218 self._recvall(sock, self.paddingLength) 219 except: 220 raise EOFError 221 222 def _sendall(sock, data): 223 """ 224 Writes data to a socket and does not return until all the data is sent. 225 """ 226 length = len(data) 227 while length: 228 try: 229 sent = sock.send(data) 230 except socket.error as e: 231 if e.errno == errno.EAGAIN: 232 select.select([], [sock], []) 233 continue 234 else: 235 raise 236 data = data[sent:] 237 length -= sent 238 _sendall = staticmethod(_sendall) 239 240 def write(self, sock): 241 """Encode and write a Record to a socket.""" 242 self.paddingLength = -self.contentLength & 7 243 244 if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, ' 245 'contentLength = %d' % 246 (sock.fileno(), self.type, self.requestId, 247 self.contentLength)) 248 249 header = struct.pack(FCGI_Header, self.version, self.type, 250 self.requestId, self.contentLength, 251 self.paddingLength) 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) 257 258class FCGIApp(object): 259 def __init__(self, command=None, connect=None, host=None, port=None, 260 filterEnviron=True): 261 if host is not None: 262 assert port is not None 263 connect=(host, port) 264 265 assert (command is not None and connect is None) or \ 266 (command is None and connect is not None) 267 268 self._command = command 269 self._connect = connect 270 271 self._filterEnviron = filterEnviron 272 273 #sock = self._getConnection() 274 #print self._fcgiGetValues(sock, ['FCGI_MAX_CONNS', 'FCGI_MAX_REQS', 'FCGI_MPXS_CONNS']) 275 #sock.close() 276 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... 282 283 sock = self._getConnection() 284 285 # Since this is going to be the only request on this connection, 286 # set the request ID to 1. 287 requestId = 1 288 289 # Begin the request 290 rec = Record(FCGI_BEGIN_REQUEST, requestId) 291 rec.contentData = struct.pack(FCGI_BeginRequestBody, FCGI_RESPONDER, 0) 292 rec.contentLength = FCGI_BeginRequestBody_LEN 293 rec.write(sock) 294 295 # Filter WSGI environ and send it as FCGI_PARAMS 296 if self._filterEnviron: 297 params = self._defaultFilterEnviron(environ) 298 else: 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, {}) 303 304 # Transfer wsgi.input to FCGI_STDIN 305 content_length = int(environ.get('CONTENT_LENGTH') or 0) 306 while True: 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) 311 rec.contentData = s 312 rec.contentLength = len(s) 313 rec.write(sock) 314 315 if not s: break 316 317 # Empty FCGI_DATA stream 318 rec = Record(FCGI_DATA, requestId) 319 rec.write(sock) 320 321 # Main loop. Process FCGI_STDOUT, FCGI_STDERR, FCGI_END_REQUEST 322 # records from the application. 323 result = [] 324 while True: 325 inrec = Record() 326 inrec.read(sock) 327 if inrec.type == FCGI_STDOUT: 328 if inrec.contentData: 329 result.append(inrec.contentData) 330 else: 331 # TODO: Should probably be pedantic and no longer 332 # accept FCGI_STDOUT records? 333 pass 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? 339 break 340 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.) 344 sock.close() 345 346 result = ''.join(result) 347 348 # Parse response headers from FCGI_STDOUT 349 status = '200 OK' 350 headers = [] 351 pos = 0 352 while True: 353 eolpos = result.find('\n', pos) 354 if eolpos < 0: break 355 line = result[pos:eolpos-1] 356 pos = eolpos + 1 357 358 # strip in case of CR. NB: This will also strip other 359 # whitespace... 360 line = line.strip() 361 362 # Empty line signifies end of headers 363 if not line: break 364 365 # TODO: Better error handling 366 header, value = line.split(':', 1) 367 header = header.strip().lower() 368 value = value.strip() 369 370 if header == 'status': 371 # Special handling of Status header 372 status = value 373 if status.find(' ') < 0: 374 # Append a dummy reason phrase if one was not provided 375 status += ' FCGIApp' 376 else: 377 headers.append((header, value)) 378 379 result = result[pos:] 380 381 # Set WSGI status, headers, and return result. 382 start_response(status, headers) 383 return [result] 384 385 def _getConnection(self): 386 if self._connect is not None: 387 # The simple case. Create a socket and connect to the 388 # application. 389 if type(self._connect) is str: 390 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 391 else: 392 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 393 sock.connect(self._connect) 394 return sock 395 396 # To be done when I have more time... 397 raise NotImplementedError('Launching and managing FastCGI programs not yet implemented') 398 399 def _fcgiGetValues(self, sock, vars): 400 # Construct FCGI_GET_VALUES record 401 outrec = Record(FCGI_GET_VALUES) 402 data = [] 403 for name in vars: 404 data.append(encode_pair(name, '')) 405 data = ''.join(data) 406 outrec.contentData = data 407 outrec.contentLength = len(data) 408 outrec.write(sock) 409 410 # Await response 411 inrec = Record() 412 inrec.read(sock) 413 result = {} 414 if inrec.type == FCGI_GET_VALUES_RESULT: 415 pos = 0 416 while pos < inrec.contentLength: 417 pos, (name, value) = decode_pair(inrec.contentData, pos) 418 result[name] = value 419 return result 420 421 def _fcgiParams(self, sock, requestId, params): 422 rec = Record(FCGI_PARAMS, requestId) 423 data = [] 424 for name,value in params.items(): 425 data.append(encode_pair(name, value)) 426 data = ''.join(data) 427 rec.contentData = data 428 rec.contentLength = len(data) 429 rec.write(sock) 430 431 _environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_', 432 'CONTENT_'] 433 _environCopies = ['SCRIPT_NAME', 'QUERY_STRING', 'AUTH_TYPE'] 434 _environRenames = {} 435 436 def _defaultFilterEnviron(self, environ): 437 result = {} 438 for n in environ.keys(): 439 for p in self._environPrefixes: 440 if n.startswith(p): 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] 446 447 return result 448 449 def _lightFilterEnviron(self, environ): 450 result = {} 451 for n in environ.keys(): 452 if n.upper() == n: 453 result[n] = environ[n] 454 return result 455 456if __name__ == '__main__': 457 from flup.server.ajp import WSGIServer 458 app = FCGIApp(connect=('localhost', 4242)) 459 #import paste.lint 460 #app = paste.lint.middleware(app) 461 WSGIServer(app).run() 462