Last active
April 6, 2017 10:17
-
-
Save davidebbo/6996671 to your computer and use it in GitHub Desktop.
wfastcgi.py file that's compatible with Python 3.x. It is preliminary and not well tested.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ############################################################################ | |
# | |
# 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 | |
try: | |
import cStringIO | |
except: | |
import io as cStringIO | |
import os | |
import traceback | |
import ctypes | |
from os import path | |
from xml.dom import minidom | |
import re | |
import datetime | |
try: | |
import thread | |
except: | |
import _thread as thread | |
__version__ = '2.0.0' | |
# 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 | |
if sys.version_info[0] >= 3: | |
# indexing into byte strings gives us an int, so | |
# ord is unnecessary on Python 3 | |
def ord(x): return x | |
def chr(x): return bytes((x, )) | |
def wsgi_decode(x): | |
return x.decode('iso-8859-1') | |
def wsgi_encode(x): | |
return x.encode('iso-8859-1') | |
else: | |
def wsgi_decode(x): | |
return x | |
def wsgi_encode(x): | |
return x | |
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 = wsgi_decode(content[offset:offset+name_len]) | |
offset += name_len | |
value = wsgi_decode(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'] = wsgi_decode(content) | |
else: | |
res['wsgi.input'] += wsgi_decode(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'] = wsgi_decode(content) | |
else: | |
res['wsgi.data'] += wsgi_decode(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 = open(log_file, 'a+') | |
try: | |
f.write(str(datetime.datetime.now())) | |
f.write(': ') | |
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 = wsgi_encode('%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 | |
sys.stdin.flush() | |
def get_environment(dir): | |
web_config = path.join(dir, 'Web.config') | |
d = {} | |
if os.path.exists(web_config): | |
try: | |
wc = open(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 is not None: | |
d[key.strip()] = value | |
finally: | |
wc.close() | |
except: | |
# unable to read file | |
log(traceback.format_exc()) | |
pass | |
return d | |
ReadDirectoryChangesW = ctypes.WinDLL('kernel32').ReadDirectoryChangesW | |
ReadDirectoryChangesW.restype = ctypes.c_bool | |
ReadDirectoryChangesW.argtypes = [ctypes.c_void_p, # HANDLE hDirectory | |
ctypes.c_void_p, # LPVOID lpBuffer | |
ctypes.c_uint32, # DWORD nBufferLength | |
ctypes.c_bool, # BOOL bWatchSubtree | |
ctypes.c_uint32, # DWORD dwNotifyFilter | |
ctypes.POINTER(ctypes.c_uint32), # LPDWORD lpBytesReturned | |
ctypes.c_void_p, # LPOVERLAPPED lpOverlapped | |
ctypes.c_void_p # LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine | |
] | |
CreateFile = ctypes.WinDLL('kernel32').CreateFileW | |
CreateFile.restype = ctypes.c_void_p | |
CreateFile.argtypes = [ctypes.c_wchar_p, # lpFilename | |
ctypes.c_uint32, # dwDesiredAccess | |
ctypes.c_uint32, # dwShareMode | |
ctypes.c_voidp, # LPSECURITY_ATTRIBUTES, | |
ctypes.c_uint32, # dwCreationDisposition, | |
ctypes.c_uint32, # dwFlagsAndAttributes, | |
ctypes.c_void_p # hTemplateFile | |
] | |
ExitProcess = ctypes.WinDLL('kernel32').ExitProcess | |
ExitProcess.restype = ctypes.c_void_p | |
ExitProcess.argtypes = [ctypes.c_uint32] | |
FILE_LIST_DIRECTORY = 1 | |
FILE_SHARE_READ = 0x00000001 | |
FILE_SHARE_WRITE = 0x00000002 | |
FILE_SHARE_DELETE = 0x00000004 | |
OPEN_EXISTING = 3 | |
FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 | |
MAX_PATH = 260 | |
FILE_NOTIFY_CHANGE_LAST_WRITE = 0x10 | |
class FILE_NOTIFY_INFORMATION(ctypes.Structure): | |
_fields_ = [('NextEntryOffset', ctypes.c_uint32), | |
('Action', ctypes.c_uint32), | |
('FileNameLength', ctypes.c_uint32), | |
('Filename', ctypes.c_wchar)] | |
def start_file_watcher(path, restartRegex): | |
if restartRegex is None: | |
restartRegex = ".*((\\.py)|(\\.config))$" | |
elif not restartRegex: | |
# restart regex set to empty string, no restart behavior | |
return | |
log('wfastcgi.py will restart when files in ' + path + ' are changed: ' + restartRegex + '\n') | |
restart = re.compile(restartRegex) | |
def watcher(path, restart): | |
the_dir = CreateFile(path, | |
FILE_LIST_DIRECTORY, | |
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | |
None, | |
OPEN_EXISTING, | |
FILE_FLAG_BACKUP_SEMANTICS, | |
None | |
) | |
buffer = ctypes.create_string_buffer(32 * 1024) | |
bytes_ret = ctypes.c_uint32() | |
while ReadDirectoryChangesW(the_dir, | |
buffer, | |
ctypes.sizeof(buffer), | |
True, | |
FILE_NOTIFY_CHANGE_LAST_WRITE, | |
ctypes.byref(bytes_ret), | |
None, | |
None): | |
cur_pointer = ctypes.addressof(buffer) | |
while True: | |
fni = ctypes.cast(cur_pointer, ctypes.POINTER(FILE_NOTIFY_INFORMATION)) | |
filename = ctypes.wstring_at(cur_pointer + 12) | |
if restart.match(filename): | |
log('wfastcgi.py exiting because ' + filename + ' has changed, matching ' + restartRegex + '\n') | |
# we call ExitProcess directly to quickly shutdown the whole process | |
# because sys.exit(0) won't have an effect on the main thread. | |
ExitProcess(0) | |
if fni.contents.NextEntryOffset == 0: | |
break | |
cur_pointer = cur_pointer + fni.contents.NextEntryOffset | |
thread.start_new_thread(watcher, (path, restart)) | |
def get_wsgi_handler(handler_name): | |
if not handler_name: | |
raise Exception('WSGI_HANDLER env var must be set') | |
module, _, callable = handler_name.rpartition('.') | |
if not module: | |
raise Exception('WSGI_HANDLER must be set to module_name.wsgi_handler, got %s' % handler_name) | |
if sys.version_info[0] == 2: | |
if isinstance(callable, unicode): | |
callable = callable.encode('ascii') | |
else: | |
if isinstance(callable, bytes): | |
callable = callable.encode('ascii') | |
if callable.endswith('()'): | |
callable = callable.rstrip('()') | |
handler = getattr(__import__(module, fromlist=[callable]), callable)() | |
else: | |
handler = getattr(__import__(module, fromlist=[callable]), callable) | |
if handler is None: | |
raise Exception('WSGI_HANDLER "' + handler_name + '" was set to None') | |
return handler | |
def read_wsgi_handler(physical_path): | |
env = get_environment(physical_path) | |
os.environ.update(env) | |
for env_name in env: | |
if env_name.lower() == 'pythonpath': | |
# expand any environment variables in the path... | |
path = env[env_name] | |
for var in os.environ: | |
pat = re.compile(re.escape('%' + var + '%'), re.IGNORECASE) | |
path = pat.sub(lambda _:os.environ[var], path) | |
for path_location in path.split(';'): | |
sys.path.append(path_location) | |
break | |
handler_ex = None | |
try: | |
handler_name = os.getenv('WSGI_HANDLER') | |
handler = get_wsgi_handler(handler_name) | |
except: | |
handler = None | |
handler_ex = sys.exc_info() | |
return env, handler, handler_ex | |
if __name__ == '__main__': | |
stdout = sys.stdin.fileno() | |
try: | |
import msvcrt | |
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) | |
except ImportError: | |
pass | |
_REQUESTS = {} | |
initialized = False | |
fatal_error = False | |
log('wfastcgi.py ' + __version__ + ' started\n') | |
if sys.version_info[0] == 2: | |
input_src = sys.stdin | |
else: | |
input_src = sys.stdin.buffer.raw | |
while fatal_error is False: | |
try: | |
record = read_fastcgi_record(input_src) | |
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 'HTTPS' in record.params 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__)) | |
errors = sys.stderr = sys.__stderr__ = record.params['wsgi.errors'] = cStringIO.StringIO() | |
output = sys.stdout = sys.__stdout__ = cStringIO.StringIO() | |
if not initialized: | |
os.chdir(physical_path) | |
env, handler, handler_ex = read_wsgi_handler(physical_path) | |
start_file_watcher(physical_path, env.get('WSGI_RESTART_FILE_REGEX')) | |
log('wfastcgi.py ' + __version__ + ' initialized\n') | |
initialized = True | |
def start_response(status, headers, exc_info = None): | |
status = 'Status: ' + status + '\r\n' | |
headers = ''.join('%s: %s\r\n' % (name, value) for name, value in headers) | |
send_response(record.req_id, FCGI_STDOUT, status + headers + '\r\n') | |
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'] | |
# PATH_INFO is not supposed to include the query parameters, so remove them | |
record.params['PATH_INFO'] = record.params['PATH_INFO'].partition('?')[0] | |
# 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'] = '' | |
if handler is None: | |
fatal_error = True | |
error_msg = ('Error while importing WSGI_HANDLER:\n\n' + | |
''.join(traceback.format_exception(*handler_ex)) + '\n\n' + | |
'StdOut: ' + output.getvalue() + '\n\n' | |
'StdErr: ' + errors.getvalue() + '\n\n') | |
log(error_msg) | |
send_response(record.req_id, FCGI_STDERR, error_msg) | |
else: | |
try: | |
for part in handler(record.params, start_response): | |
if part: | |
send_response(record.req_id, FCGI_STDOUT, wsgi_decode(part)) | |
except: | |
log('Exception from handler: ' + traceback.format_exc()) | |
send_response(record.req_id, FCGI_STDERR, errors.getvalue() + '\n\n' + traceback.format_exc()) | |
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('Unhandled exception in wfastcgi.py: ' + traceback.format_exc()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment