Modules/Azure/Scaffolding/Python/WebRole/wfastcgi.py (252 lines of code) (raw):

# ############################################################################ # # Copyright (c) Microsoft Corporation. # # This source code is subject to terms and conditions of the Apache License, Version 2.0. A # copy of the license can be found in the License.html file at the root of this distribution. If # you cannot locate the Apache License, Version 2.0, please send an email to # vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound # by the terms of the Apache License, Version 2.0. # # You must not remove this notice, or any other, from this software. # # ########################################################################### import sys import struct import cStringIO import os import traceback from os import path from xml.dom import minidom # http://www.fastcgi.com/devkit/doc/fcgi-spec.html#S3 FCGI_VERSION_1 = 1 FCGI_HEADER_LEN = 8 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" class FastCgiRecord(object): """Represents a FastCgiRecord. Encapulates the type, role, flags. Holds onto the params which we will receive and update later.""" def __init__(self, type, req_id, role, flags): self.type = type self.req_id = req_id self.role = role self.flags = flags self.params = {} def __repr__(self): return '<FastCgiRecord(%d, %d, %d, %d)>' % (self.type, self.req_id, self.role, self.flags) #typedef struct { # unsigned char version; # unsigned char type; # unsigned char requestIdB1; # unsigned char requestIdB0; # unsigned char contentLengthB1; # unsigned char contentLengthB0; # unsigned char paddingLength; # unsigned char reserved; # unsigned char contentData[contentLength]; # unsigned char paddingData[paddingLength]; #} FCGI_Record; def read_fastcgi_record(input): """reads the main fast cgi record""" data = input.read(8) # read record content_size = ord(data[4]) << 8 | ord(data[5]) content = input.read(content_size) # read content input.read(ord(data[6])) # read padding if ord(data[0]) != FCGI_VERSION_1: raise Exception('Unknown fastcgi version ' + str(data[0])) req_id = (ord(data[2]) << 8) | ord(data[3]) reqtype = ord(data[1]) processor = REQUEST_PROCESSORS.get(reqtype) if processor is None: # unknown type requested, send response send_response(req_id, FCGI_UNKNOWN_TYPE, data[1] + '\0' * 7) return None return processor(req_id, content) def read_fastcgi_begin_request(req_id, content): """reads the begin request body and updates our _REQUESTS table to include the new request""" # typedef struct { # unsigned char roleB1; # unsigned char roleB0; # unsigned char flags; # unsigned char reserved[5]; # } FCGI_BeginRequestBody; # TODO: Ignore request if it exists res = FastCgiRecord( FCGI_BEGIN_REQUEST, req_id, (ord(content[0]) << 8) | ord(content[1]), # role ord(content[2]), # flags ) _REQUESTS[req_id] = res def read_fastcgi_keyvalue_pairs(content, offset): """Reads a FastCGI key/value pair stream""" name_len = ord(content[offset]) if (name_len & 0x80) != 0: name_full_len = chr(name_len & ~0x80) + content[offset + 1:offset+4] name_len = int_struct.unpack(name_full_len)[0] offset += 4 else: offset += 1 value_len = ord(content[offset]) if (value_len & 0x80) != 0: value_full_len = chr(value_len & ~0x80) + content[offset+1:offset+4] value_len = int_struct.unpack(value_full_len)[0] offset += 4 else: offset += 1 name = content[offset:offset+name_len] offset += name_len value = content[offset:offset+value_len] offset += value_len return offset, name, value def write_name_len(io, name): """Writes the length of a single name for a key or value in a key/value stream""" if len(name) <= 0x7f: io.write(chr(len(name))) else: io.write(int_struct.pack(len(name))) def write_fastcgi_keyvalue_pairs(pairs): """creates a FastCGI key/value stream and returns it as a string""" res = cStringIO.StringIO() for key, value in pairs.iteritems(): write_name_len(res, key) write_name_len(res, value) res.write(key) res.write(value) return res.getvalue() def read_fastcgi_params(req_id, content): if not content: return None offset = 0 res = _REQUESTS[req_id].params while offset < len(content): offset, name, value = read_fastcgi_keyvalue_pairs(content, offset) res[name] = value def read_fastcgi_input(req_id, content): """reads FastCGI std-in and stores it in wsgi.input passed in the wsgi environment array""" res = _REQUESTS[req_id].params if 'wsgi.input' not in res: res['wsgi.input'] = content else: res['wsgi.input'] += content if not content: # we've hit the end of the input stream, time to process input... return _REQUESTS[req_id] def read_fastcgi_data(req_id, content): """reads FastCGI data stream and publishes it as wsgi.data""" res = _REQUESTS[req_id].params if 'wsgi.data' not in res: res['wsgi.data'] = content else: res['wsgi.data'] += content def read_fastcgi_abort_request(req_id, content): """reads the wsgi abort request, which we ignore, we'll send the finish execution request anyway...""" pass def read_fastcgi_get_values(req_id, content): """reads the fastcgi request to get parameter values, and immediately responds""" offset = 0 request = {} while offset < len(content): offset, name, value = read_fastcgi_keyvalue_pairs(content, offset) request[name] = value response = {} if FCGI_MAX_CONNS in request: response[FCGI_MAX_CONNS] = '1' if FCGI_MAX_REQS in request: response[FCGI_MAX_REQS] = '1' if FCGI_MPXS_CONNS in request: response[FCGI_MPXS_CONNS] = '0' send_response(req_id, FCGI_GET_VALUES_RESULT, write_fastcgi_keyvalue_pairs(response)) # Formatting of 4-byte ints in network order int_struct = struct.Struct('!i') # Our request processors for different FastCGI protocol requests. Only # the requests which we receive are defined here. REQUEST_PROCESSORS = { FCGI_BEGIN_REQUEST : read_fastcgi_begin_request, FCGI_ABORT_REQUEST : read_fastcgi_abort_request, FCGI_PARAMS : read_fastcgi_params, FCGI_STDIN : read_fastcgi_input, FCGI_DATA : read_fastcgi_data, FCGI_GET_VALUES : read_fastcgi_get_values } def log(txt): """Logs fatal errors to a log file if WSGI_LOG env var is defined""" log_file = os.environ.get('WSGI_LOG') if log_file: with file(log_file, 'a+') as f: f.write(txt) def send_response(id, resp_type, content, streaming = True): """sends a response w/ the given id, type, and content to the server. If the content is streaming then an empty record is sent at the end to terminate the stream""" offset = 0 while 1: if id < 256: id_0 = 0 id_1 = id else: id_0 = id >> 8 id_1 = id & 0xff # content len, padding len, content len_remaining = len(content) - offset if len_remaining > 65535: len_0 = 0xff len_1 = 0xff content_str = content[offset:offset+65535] offset += 65535 else: len_0 = len_remaining >> 8 len_1 = len_remaining & 0xff content_str = content[offset:] offset += len_remaining data = '%c%c%c%c%c%c%c%c%s' % ( FCGI_VERSION_1, # version resp_type, # type id_0, # requestIdB1 id_1, # requestIdB0 len_0, # contentLengthB1 len_1, # contentLengthB0 0, # paddingLength 0, # reserved content_str) os.write(stdout, data) if len_remaining == 0 or not streaming: break def update_environment(): cur_dir = path.dirname(path.dirname(__file__)) web_config = path.join(cur_dir, 'Web.config') if os.path.exists(web_config): try: with file(web_config) as wc: doc = minidom.parse(wc) config = doc.getElementsByTagName('configuration') for configSection in config: appSettings = configSection.getElementsByTagName('appSettings') for appSettingsSection in appSettings: values = appSettingsSection.getElementsByTagName('add') for curAdd in values: key = curAdd.getAttribute('key') value = curAdd.getAttribute('value') if key and value: os.environ[key] = value except: # unable to read file log(traceback.format_exc()) pass if __name__ == '__main__': handler_name = os.getenv('WSGI_HANDLER', 'django.core.handlers.wsgi.WSGIHandler()') module, callable = handler_name.rsplit('.', 1) if callable.endswith('()'): callable = callable.rstrip('()') handler = getattr(__import__(module, fromlist=[callable]), callable)() else: handler = getattr(__import__(module, fromlist=[callable]), callable) stdout = sys.stdin.fileno() try: import msvcrt msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) except ImportError: pass update_environment() _REQUESTS = {} while True: try: record = read_fastcgi_record(sys.stdin) if record: record.params['wsgi.input'] = cStringIO.StringIO(record.params['wsgi.input']) record.params['wsgi.version'] = (1,0) record.params['wsgi.url_scheme'] = 'https' if record.params.has_key('HTTPS') and record.params['HTTPS'].lower() == 'on' else 'http' record.params['wsgi.multiprocess'] = True record.params['wsgi.multithread'] = False record.params['wsgi.run_once'] = False def start_response(status, headers, exc_info = None): global response_headers, status_line response_headers = headers status_line = status errors = sys.stderr = sys.__stderr__ = record.params['wsgi.errors'] = cStringIO.StringIO() sys.stdout = sys.__stdout__ = cStringIO.StringIO() record.params['SCRIPT_NAME'] = '' try: response = ''.join(handler(record.params, start_response)) except: send_response(record.req_id, FCGI_STDERR, errors.getvalue()) else: status = 'Status: ' + status_line + '\r\n' headers = ''.join('%s: %s\r\n' % (name, value) for name, value in response_headers) full_response = status + headers + '\r\n' + response send_response(record.req_id, FCGI_STDOUT, full_response) # for testing of throughput of fastcgi handler vs static pages #send_response(record.req_id, FCGI_STDOUT, 'Content-type: text/html\r\n\r\n\r\n<html>\n<body>bar</body></html>') send_response(record.req_id, FCGI_END_REQUEST, '\x00\x00\x00\x00\x00\x00\x00\x00', streaming=False) del _REQUESTS[record.req_id] except: log(traceback.format_exc())