|
# ############################################################################ |
|
# |
|
# 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 |
|
# [email protected]. 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; |
|
|
|
class _ExitException(Exception): |
|
pass |
|
|
|
def read_fastcgi_record(input): |
|
"""reads the main fast cgi record""" |
|
data = input.read(8) # read record |
|
if not data: |
|
# no more data, our other process must have died... |
|
raise _ExitException() |
|
|
|
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: |
|
f = file(log_file, 'a+') |
|
try: |
|
f.write(txt) |
|
finally: |
|
f.close() |
|
|
|
|
|
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 get_environment(dir): |
|
web_config = path.join(dir, 'Web.config') |
|
|
|
d = {} |
|
|
|
if os.path.exists(web_config): |
|
try: |
|
wc = file(web_config) |
|
try: |
|
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: |
|
d[key] = value |
|
finally: |
|
wc.close() |
|
except: |
|
# unable to read file |
|
log(traceback.format_exc()) |
|
pass |
|
return d |
|
|
|
|
|
if __name__ == '__main__': |
|
stdout = sys.stdin.fileno() |
|
try: |
|
import msvcrt |
|
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) |
|
except ImportError: |
|
pass |
|
|
|
_REQUESTS = {} |
|
|
|
initialized = False |
|
|
|
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 |
|
|
|
physical_path = record.params.get('DOCUMENT_ROOT', path.dirname(__file__)) |
|
|
|
if not initialized: |
|
env = get_environment(physical_path) |
|
os.environ.update(env) |
|
sys.path.append(physical_path) # TODO: line added for PYTHONPATH |
|
for env_name in env: |
|
if env_name.lower() == 'pythonpath': |
|
for path_location in env[env_name].split(';'): |
|
sys.path.append(path_location) |
|
break |
|
|
|
handler_name = os.getenv('WSGI_HANDLER') |
|
if not handler_name: |
|
sys.stderr.write('WSGI_HANDLER env var must be set') |
|
sys.exit(1) |
|
|
|
module, _, callable = handler_name.rpartition('.') |
|
if not module: |
|
sys.stderr.write('WSGI_HANDLER must be set to module_name.wsgi_handler, got %s' % handler_name) |
|
sys.exit(2) |
|
|
|
if callable.endswith('()'): |
|
callable = callable.rstrip('()') |
|
handler = getattr(__import__(module, fromlist=[callable]), callable)() |
|
else: |
|
handler = getattr(__import__(module, fromlist=[callable]), callable) |
|
initialized = True |
|
|
|
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() |
|
|
|
os.environ.update(env) |
|
if 'HTTP_X_ORIGINAL_URL' in record.params: |
|
# We've been re-written for shared FastCGI hosting, send the original URL as the PATH_INFO. |
|
record.params['PATH_INFO'] = record.params['HTTP_X_ORIGINAL_URL'] |
|
|
|
# SCRIPT_NAME + PATH_INFO is supposed to be the full path http://www.python.org/dev/peps/pep-0333/ |
|
# but by default (http://msdn.microsoft.com/en-us/library/ms525840(v=vs.90).aspx) IIS is sending us |
|
# the full URL in PATH_INFO, so we need to clear the script name here |
|
if 'AllowPathInfoForScriptMappings' not in os.environ: |
|
record.params['SCRIPT_NAME'] = '' |
|
|
|
try: |
|
response = ''.join(handler(record.params, start_response)) |
|
except: |
|
send_response(record.req_id, FCGI_STDERR, errors.getvalue() + '\n\n' + traceback.format_exc()) |
|
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) |
|
|
|
send_response(record.req_id, FCGI_END_REQUEST, '\x00\x00\x00\x00\x00\x00\x00\x00', streaming=False) |
|
del _REQUESTS[record.req_id] |
|
except _ExitException: |
|
break |
|
except: |
|
log(traceback.format_exc()) |