Skip to content

Instantly share code, notes, and snippets.

@davidebbo
Last active April 6, 2017 10:17
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davidebbo/6996671 to your computer and use it in GitHub Desktop.
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.
# ############################################################################
#
# 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
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