Uploaded Test files

This commit is contained in:
Batuhan Berk Başoğlu 2020-11-12 11:05:57 -05:00
parent f584ad9d97
commit 2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions

Binary file not shown.

View file

@ -0,0 +1,7 @@
A Python ISAPI extension. Contributed by Phillip Frantz, and is
Copyright 2002-2003 by Blackdog Software Pty Ltd.
See the 'samples' directory, and particularly samples\README.txt
You can find documentation in the PyWin32.chm file that comes with pywin32 -
you can open this from Pythonwin->Help, or from the start menu.

View file

@ -0,0 +1,33 @@
# The Python ISAPI package.
# Exceptions thrown by the DLL framework.
class ISAPIError(Exception):
def __init__(self, errno, strerror = None, funcname = None):
# named attributes match IOError etc.
self.errno = errno
self.strerror = strerror
self.funcname = funcname
Exception.__init__(self, errno, strerror, funcname)
def __str__(self):
if self.strerror is None:
try:
import win32api
self.strerror = win32api.FormatMessage(self.errno).strip()
except:
self.strerror = "no error message is available"
# str() looks like a win32api error.
return str( (self.errno, self.strerror, self.funcname) )
class FilterError(ISAPIError):
pass
class ExtensionError(ISAPIError):
pass
# A little development aid - a filter or extension callback function can
# raise one of these exceptions, and the handler module will be reloaded.
# This means you can change your code without restarting IIS.
# After a reload, your filter/extension will have the GetFilterVersion/
# GetExtensionVersion function called, but with None as the first arg.
class InternalReloadException(Exception):
pass

View file

@ -0,0 +1,92 @@
<!-- NOTE: This HTML is displayed inside the CHM file - hence some hrefs
will only work in that environment
-->
<HTML>
<BODY>
<TITLE>Introduction to Python ISAPI support</TITLE>
<h2>Introduction to Python ISAPI support</h2>
<h3>See also</h3>
<ul>
<li><a href="/isapi_modules.html">The isapi related modules</a>
</li>
<li><a href="/isapi_objects.html">The isapi related objects</a>
</li>
</ul>
<p><i>Note: if you are viewing this documentation directly from disk,
most links in this document will fail - you can also find this document in the
CHM file that comes with pywin32, where the links will work</i>
<h3>Introduction</h3>
This documents Python support for hosting ISAPI exensions and filters inside
Microsoft Internet Information Server (IIS). It assumes a basic understanding
of the ISAPI filter and extension mechanism.
<p>
In summary, to implement a filter or extension, you provide a Python module
which defines a Filter and/or Extension class. Once your class has been
loaded, IIS/ISAPI will, via an extension DLL, call methods on your class.
<p>
A filter and a class instance need only provide 3 methods - for filters they
are called <code>GetFilterVersion</code>, <code>HttpFilterProc</code> and
<code>TerminateFilter</code>. For extensions they
are named <code>GetExtensionVersion</code>, <code>HttpExtensionProc</code> and
<code>TerminateExtension</code>. If you are familiar with writing ISAPI
extensions in C/C++, these names and their purpose will be familiar.
<p>
Most of the work is done in the <code>HttpFilterProc</code> and
<code>HttpExtensionProc</code> methods. These both take a single
parameter - an <a href="/HTTP_FILTER_CONTEXT.html">HTTP_FILTER_CONTEXT</a> and
<a href="/EXTENSION_CONTROL_BLOCK.html">EXTENSION_CONTROL_BLOCK</a>
object respectively.
<p>
In addition to these components, there is an 'isapi' package, containing
support facilities (base-classes, exceptions, etc) which can be leveraged
by the extension.
<h4>Base classes</h4>
There are a number of base classes provided to make writing extensions a little
simpler. Of particular note is <code>isapi.threaded_extension.ThreadPoolExtension</code>.
This implements a thread-pool and informs IIS that the request is progressing
in the background. Your sub-class need only provide a <code>Dispatch</code>
method, which is called on one of the worker threads rather than the thread
that the request came in on.
<p>
There is base-class for a filter in <code>isapi.simple</code>, but there is no
equivilent threaded filter - filters work under a different model, where
background processing is not possible.
<h4>Samples</h4>
Please see the <code>isapi/samples</code> directory for some sample filters
and extensions.
<H3>Implementation</H3>
A Python ISAPI filter extension consists of 2 main components:
<UL>
<LI>A DLL used by ISAPI to interface with Python.</LI>
<LI>A Python script used by that DLL to implement the filter or extension
functionality</LI>
</UL>
<h4>Extension DLL</h4>
The DLL is usually managed automatically by the isapi.install module. As the
Python script for the extension is installed, a generic DLL provided with
the isapi package is installed next to the script, and IIS configured to
use this DLL.
<p>
The name of the DLL always has the same base name as the Python script, but
with a leading underscore (_), and an extension of .dll. For example, the
sample "redirector.py" will, when installed, have "_redirector.dll" created
in the same directory.
<p/>
The Python script may provide 2 entry points - methods named __FilterFactory__
and __ExtensionFactory__, both taking no arguments and returning a filter or
extension object.
<h3>Using py2exe and the isapi package</h3>
You can instruct py2exe to create a 'frozen' Python ISAPI filter/extension.
In this case, py2exe will create a package with everything you need in one
directory, and the Python source file embedded in the .zip file.
<p>
In general, you will want to build a seperate installation executable along
with the ISAPI extension. This executable will be built from the same script.
See the ISAPI sample in the py2exe distribution.

View file

@ -0,0 +1,730 @@
"""Installation utilities for Python ISAPI filters and extensions."""
# this code adapted from "Tomcat JK2 ISAPI redirector", part of Apache
# Created July 2004, Mark Hammond.
import sys, os, imp, shutil, stat
import operator
from win32com.client import GetObject, Dispatch
from win32com.client.gencache import EnsureModule, EnsureDispatch
import win32api
import pythoncom
import winerror
import traceback
_APP_INPROC = 0
_APP_OUTPROC = 1
_APP_POOLED = 2
_IIS_OBJECT = "IIS://LocalHost/W3SVC"
_IIS_SERVER = "IIsWebServer"
_IIS_WEBDIR = "IIsWebDirectory"
_IIS_WEBVIRTUALDIR = "IIsWebVirtualDir"
_IIS_FILTERS = "IIsFilters"
_IIS_FILTER = "IIsFilter"
_DEFAULT_SERVER_NAME = "Default Web Site"
_DEFAULT_HEADERS = "X-Powered-By: Python"
_DEFAULT_PROTECTION = _APP_POOLED
# Default is for 'execute' only access - ie, only the extension
# can be used. This can be overridden via your install script.
_DEFAULT_ACCESS_EXECUTE = True
_DEFAULT_ACCESS_READ = False
_DEFAULT_ACCESS_WRITE = False
_DEFAULT_ACCESS_SCRIPT = False
_DEFAULT_CONTENT_INDEXED = False
_DEFAULT_ENABLE_DIR_BROWSING = False
_DEFAULT_ENABLE_DEFAULT_DOC = False
_extensions = [ext for ext, _, _ in imp.get_suffixes()]
is_debug_build = '_d.pyd' in _extensions
this_dir = os.path.abspath(os.path.dirname(__file__))
class FilterParameters:
Name = None
Description = None
Path = None
Server = None
# Params that control if/how AddExtensionFile is called.
AddExtensionFile = True
AddExtensionFile_Enabled = True
AddExtensionFile_GroupID = None # defaults to Name
AddExtensionFile_CanDelete = True
AddExtensionFile_Description = None # defaults to Description.
def __init__(self, **kw):
self.__dict__.update(kw)
class VirtualDirParameters:
Name = None # Must be provided.
Description = None # defaults to Name
AppProtection = _DEFAULT_PROTECTION
Headers = _DEFAULT_HEADERS
Path = None # defaults to WWW root.
Type = _IIS_WEBVIRTUALDIR
AccessExecute = _DEFAULT_ACCESS_EXECUTE
AccessRead = _DEFAULT_ACCESS_READ
AccessWrite = _DEFAULT_ACCESS_WRITE
AccessScript = _DEFAULT_ACCESS_SCRIPT
ContentIndexed = _DEFAULT_CONTENT_INDEXED
EnableDirBrowsing = _DEFAULT_ENABLE_DIR_BROWSING
EnableDefaultDoc = _DEFAULT_ENABLE_DEFAULT_DOC
DefaultDoc = None # Only set in IIS if not None
ScriptMaps = []
ScriptMapUpdate = "end" # can be 'start', 'end', 'replace'
Server = None
def __init__(self, **kw):
self.__dict__.update(kw)
def is_root(self):
"This virtual directory is a root directory if parent and name are blank"
parent, name = self.split_path()
return not parent and not name
def split_path(self):
return split_path(self.Name)
class ScriptMapParams:
Extension = None
Module = None
Flags = 5
Verbs = ""
# Params that control if/how AddExtensionFile is called.
AddExtensionFile = True
AddExtensionFile_Enabled = True
AddExtensionFile_GroupID = None # defaults to Name
AddExtensionFile_CanDelete = True
AddExtensionFile_Description = None # defaults to Description.
def __init__(self, **kw):
self.__dict__.update(kw)
def __str__(self):
"Format this parameter suitable for IIS"
items = [self.Extension, self.Module, self.Flags]
# IIS gets upset if there is a trailing verb comma, but no verbs
if self.Verbs:
items.append(self.Verbs)
items = [str(item) for item in items]
return ','.join(items)
class ISAPIParameters:
ServerName = _DEFAULT_SERVER_NAME
# Description = None
Filters = []
VirtualDirs = []
def __init__(self, **kw):
self.__dict__.update(kw)
verbose = 1 # The level - 0 is quiet.
def log(level, what):
if verbose >= level:
print(what)
# Convert an ADSI COM exception to the Win32 error code embedded in it.
def _GetWin32ErrorCode(com_exc):
hr = com_exc.hresult
# If we have more details in the 'excepinfo' struct, use it.
if com_exc.excepinfo:
hr = com_exc.excepinfo[-1]
if winerror.HRESULT_FACILITY(hr) != winerror.FACILITY_WIN32:
raise
return winerror.SCODE_CODE(hr)
class InstallationError(Exception): pass
class ItemNotFound(InstallationError): pass
class ConfigurationError(InstallationError): pass
def FindPath(options, server, name):
if name.lower().startswith("iis://"):
return name
else:
if name and name[0] != "/":
name = "/"+name
return FindWebServer(options, server)+"/ROOT"+name
def LocateWebServerPath(description):
"""
Find an IIS web server whose name or comment matches the provided
description (case-insensitive).
>>> LocateWebServerPath('Default Web Site') # doctest: +SKIP
or
>>> LocateWebServerPath('1') #doctest: +SKIP
"""
assert len(description) >= 1, "Server name or comment is required"
iis = GetObject(_IIS_OBJECT)
description = description.lower().strip()
for site in iis:
# Name is generally a number, but no need to assume that.
site_attributes = [getattr(site, attr, "").lower().strip()
for attr in ("Name", "ServerComment")]
if description in site_attributes:
return site.AdsPath
msg = "No web sites match the description '%s'" % description
raise ItemNotFound(msg)
def GetWebServer(description = None):
"""
Load the web server instance (COM object) for a given instance
or description.
If None is specified, the default website is retrieved (indicated
by the identifier 1.
"""
description = description or "1"
path = LocateWebServerPath(description)
server = LoadWebServer(path)
return server
def LoadWebServer(path):
try:
server = GetObject(path)
except pythoncom.com_error as details:
msg = details.strerror
if exc.excepinfo and exc.excepinfo[2]:
msg = exc.excepinfo[2]
msg = "WebServer %s: %s" % (path, msg)
raise ItemNotFound(msg)
return server
def FindWebServer(options, server_desc):
"""
Legacy function to allow options to define a .server property
to override the other parameter. Use GetWebServer instead.
"""
# options takes precedence
server_desc = options.server or server_desc
# make sure server_desc is unicode (could be mbcs if passed in
# sys.argv).
if server_desc and not isinstance(server_desc, str):
server_desc = server_desc.decode('mbcs')
# get the server (if server_desc is None, the default site is acquired)
server = GetWebServer(server_desc)
return server.adsPath
def split_path(path):
"""
Get the parent path and basename.
>>> split_path('/')
['', '']
>>> split_path('')
['', '']
>>> split_path('foo')
['', 'foo']
>>> split_path('/foo')
['', 'foo']
>>> split_path('/foo/bar')
['/foo', 'bar']
>>> split_path('foo/bar')
['/foo', 'bar']
"""
if not path.startswith('/'): path = '/' + path
return path.rsplit('/', 1)
def _CreateDirectory(iis_dir, name, params):
# We used to go to lengths to keep an existing virtual directory
# in place. However, in some cases the existing directories got
# into a bad state, and an update failed to get them working.
# So we nuke it first. If this is a problem, we could consider adding
# a --keep-existing option.
try:
# Also seen the Class change to a generic IISObject - so nuke
# *any* existing object, regardless of Class
assert name.strip("/"), "mustn't delete the root!"
iis_dir.Delete('', name)
log(2, "Deleted old directory '%s'" % (name,))
except pythoncom.com_error:
pass
newDir = iis_dir.Create(params.Type, name)
log(2, "Creating new directory '%s' in %s..." % (name,iis_dir.Name))
friendly = params.Description or params.Name
newDir.AppFriendlyName = friendly
# Note that the new directory won't be visible in the IIS UI
# unless the directory exists on the filesystem.
try:
path = params.Path or iis_dir.Path
newDir.Path = path
except AttributeError:
# If params.Type is IIS_WEBDIRECTORY, an exception is thrown
pass
newDir.AppCreate2(params.AppProtection)
# XXX - note that these Headers only work in IIS6 and earlier. IIS7
# only supports them on the w3svc node - not even on individial sites,
# let alone individual extensions in the site!
if params.Headers:
newDir.HttpCustomHeaders = params.Headers
log(2, "Setting directory options...")
newDir.AccessExecute = params.AccessExecute
newDir.AccessRead = params.AccessRead
newDir.AccessWrite = params.AccessWrite
newDir.AccessScript = params.AccessScript
newDir.ContentIndexed = params.ContentIndexed
newDir.EnableDirBrowsing = params.EnableDirBrowsing
newDir.EnableDefaultDoc = params.EnableDefaultDoc
if params.DefaultDoc is not None:
newDir.DefaultDoc = params.DefaultDoc
newDir.SetInfo()
return newDir
def CreateDirectory(params, options):
_CallHook(params, "PreInstall", options)
if not params.Name:
raise ConfigurationError("No Name param")
parent, name = params.split_path()
target_dir = GetObject(FindPath(options, params.Server, parent))
if not params.is_root():
target_dir = _CreateDirectory(target_dir, name, params)
AssignScriptMaps(params.ScriptMaps, target_dir, params.ScriptMapUpdate)
_CallHook(params, "PostInstall", options, target_dir)
log(1, "Configured Virtual Directory: %s" % (params.Name,))
return target_dir
def AssignScriptMaps(script_maps, target, update='replace'):
"""Updates IIS with the supplied script map information.
script_maps is a list of ScriptMapParameter objects
target is an IIS Virtual Directory to assign the script maps to
update is a string indicating how to update the maps, one of ('start',
'end', or 'replace')
"""
# determine which function to use to assign script maps
script_map_func = '_AssignScriptMaps' + update.capitalize()
try:
script_map_func = eval(script_map_func)
except NameError:
msg = "Unknown ScriptMapUpdate option '%s'" % update
raise ConfigurationError(msg)
# use the str method to format the script maps for IIS
script_maps = [str(s) for s in script_maps]
# call the correct function
script_map_func(target, script_maps)
target.SetInfo()
def get_unique_items(sequence, reference):
"Return items in sequence that can't be found in reference."
return tuple([item for item in sequence if item not in reference])
def _AssignScriptMapsReplace(target, script_maps):
target.ScriptMaps = script_maps
def _AssignScriptMapsEnd(target, script_maps):
unique_new_maps = get_unique_items(script_maps, target.ScriptMaps)
target.ScriptMaps = target.ScriptMaps + unique_new_maps
def _AssignScriptMapsStart(target, script_maps):
unique_new_maps = get_unique_items(script_maps, target.ScriptMaps)
target.ScriptMaps = unique_new_maps + target.ScriptMaps
def CreateISAPIFilter(filterParams, options):
server = FindWebServer(options, filterParams.Server)
_CallHook(filterParams, "PreInstall", options)
try:
filters = GetObject(server+"/Filters")
except pythoncom.com_error as exc:
# Brand new sites don't have the '/Filters' collection - create it.
# Any errors other than 'not found' we shouldn't ignore.
if winerror.HRESULT_FACILITY(exc.hresult) != winerror.FACILITY_WIN32 or \
winerror.HRESULT_CODE(exc.hresult) != winerror.ERROR_PATH_NOT_FOUND:
raise
server_ob = GetObject(server)
filters = server_ob.Create(_IIS_FILTERS, "Filters")
filters.FilterLoadOrder = ""
filters.SetInfo()
# As for VirtualDir, delete an existing one.
assert filterParams.Name.strip("/"), "mustn't delete the root!"
try:
filters.Delete(_IIS_FILTER, filterParams.Name)
log(2, "Deleted old filter '%s'" % (filterParams.Name,))
except pythoncom.com_error:
pass
newFilter = filters.Create(_IIS_FILTER, filterParams.Name)
log(2, "Created new ISAPI filter...")
assert os.path.isfile(filterParams.Path)
newFilter.FilterPath = filterParams.Path
newFilter.FilterDescription = filterParams.Description
newFilter.SetInfo()
load_order = [b.strip() for b in filters.FilterLoadOrder.split(",") if b]
if filterParams.Name not in load_order:
load_order.append(filterParams.Name)
filters.FilterLoadOrder = ",".join(load_order)
filters.SetInfo()
_CallHook(filterParams, "PostInstall", options, newFilter)
log (1, "Configured Filter: %s" % (filterParams.Name,))
return newFilter
def DeleteISAPIFilter(filterParams, options):
_CallHook(filterParams, "PreRemove", options)
server = FindWebServer(options, filterParams.Server)
ob_path = server+"/Filters"
try:
filters = GetObject(ob_path)
except pythoncom.com_error as details:
# failure to open the filters just means a totally clean IIS install
# (IIS5 at least has no 'Filters' key when freshly installed).
log(2, "ISAPI filter path '%s' did not exist." % (ob_path,))
return
try:
assert filterParams.Name.strip("/"), "mustn't delete the root!"
filters.Delete(_IIS_FILTER, filterParams.Name)
log(2, "Deleted ISAPI filter '%s'" % (filterParams.Name,))
except pythoncom.com_error as details:
rc = _GetWin32ErrorCode(details)
if rc != winerror.ERROR_PATH_NOT_FOUND:
raise
log(2, "ISAPI filter '%s' did not exist." % (filterParams.Name,))
# Remove from the load order
load_order = [b.strip() for b in filters.FilterLoadOrder.split(",") if b]
if filterParams.Name in load_order:
load_order.remove(filterParams.Name)
filters.FilterLoadOrder = ",".join(load_order)
filters.SetInfo()
_CallHook(filterParams, "PostRemove", options)
log (1, "Deleted Filter: %s" % (filterParams.Name,))
def _AddExtensionFile(module, def_groupid, def_desc, params, options):
group_id = params.AddExtensionFile_GroupID or def_groupid
desc = params.AddExtensionFile_Description or def_desc
try:
ob = GetObject(_IIS_OBJECT)
ob.AddExtensionFile(module,
params.AddExtensionFile_Enabled,
group_id,
params.AddExtensionFile_CanDelete,
desc)
log(2, "Added extension file '%s' (%s)" % (module, desc))
except (pythoncom.com_error, AttributeError) as details:
# IIS5 always fails. Probably should upgrade this to
# complain more loudly if IIS6 fails.
log(2, "Failed to add extension file '%s': %s" % (module, details))
def AddExtensionFiles(params, options):
"""Register the modules used by the filters/extensions as a trusted
'extension module' - required by the default IIS6 security settings."""
# Add each module only once.
added = {}
for vd in params.VirtualDirs:
for smp in vd.ScriptMaps:
if smp.Module not in added and smp.AddExtensionFile:
_AddExtensionFile(smp.Module, vd.Name, vd.Description, smp,
options)
added[smp.Module] = True
for fd in params.Filters:
if fd.Path not in added and fd.AddExtensionFile:
_AddExtensionFile(fd.Path, fd.Name, fd.Description, fd, options)
added[fd.Path] = True
def _DeleteExtensionFileRecord(module, options):
try:
ob = GetObject(_IIS_OBJECT)
ob.DeleteExtensionFileRecord(module)
log(2, "Deleted extension file record for '%s'" % module)
except (pythoncom.com_error, AttributeError) as details:
log(2, "Failed to remove extension file '%s': %s" % (module, details))
def DeleteExtensionFileRecords(params, options):
deleted = {} # only remove each .dll once.
for vd in params.VirtualDirs:
for smp in vd.ScriptMaps:
if smp.Module not in deleted and smp.AddExtensionFile:
_DeleteExtensionFileRecord(smp.Module, options)
deleted[smp.Module] = True
for filter_def in params.Filters:
if filter_def.Path not in deleted and filter_def.AddExtensionFile:
_DeleteExtensionFileRecord(filter_def.Path, options)
deleted[filter_def.Path] = True
def CheckLoaderModule(dll_name):
suffix = ""
if is_debug_build: suffix = "_d"
template = os.path.join(this_dir,
"PyISAPI_loader" + suffix + ".dll")
if not os.path.isfile(template):
raise ConfigurationError(
"Template loader '%s' does not exist" % (template,))
# We can't do a simple "is newer" check, as the DLL is specific to the
# Python version. So we check the date-time and size are identical,
# and skip the copy in that case.
src_stat = os.stat(template)
try:
dest_stat = os.stat(dll_name)
except os.error:
same = 0
else:
same = src_stat[stat.ST_SIZE]==dest_stat[stat.ST_SIZE] and \
src_stat[stat.ST_MTIME]==dest_stat[stat.ST_MTIME]
if not same:
log(2, "Updating %s->%s" % (template, dll_name))
shutil.copyfile(template, dll_name)
shutil.copystat(template, dll_name)
else:
log(2, "%s is up to date." % (dll_name,))
def _CallHook(ob, hook_name, options, *extra_args):
func = getattr(ob, hook_name, None)
if func is not None:
args = (ob,options) + extra_args
func(*args)
def Install(params, options):
_CallHook(params, "PreInstall", options)
for vd in params.VirtualDirs:
CreateDirectory(vd, options)
for filter_def in params.Filters:
CreateISAPIFilter(filter_def, options)
AddExtensionFiles(params, options)
_CallHook(params, "PostInstall", options)
def RemoveDirectory(params, options):
if params.is_root():
return
try:
directory = GetObject(FindPath(options, params.Server, params.Name))
except pythoncom.com_error as details:
rc = _GetWin32ErrorCode(details)
if rc != winerror.ERROR_PATH_NOT_FOUND:
raise
log(2, "VirtualDirectory '%s' did not exist" % params.Name)
directory = None
if directory is not None:
# Be robust should IIS get upset about unloading.
try:
directory.AppUnLoad()
except:
exc_val = sys.exc_info()[1]
log(2, "AppUnLoad() for %s failed: %s" % (params.Name, exc_val))
# Continue trying to delete it.
try:
parent = GetObject(directory.Parent)
parent.Delete(directory.Class, directory.Name)
log (1, "Deleted Virtual Directory: %s" % (params.Name,))
except:
exc_val = sys.exc_info()[1]
log(1, "Failed to remove directory %s: %s" % (params.Name, exc_val))
def RemoveScriptMaps(vd_params, options):
"Remove script maps from the already installed virtual directory"
parent, name = vd_params.split_path()
target_dir = GetObject(FindPath(options, vd_params.Server, parent))
installed_maps = list(target_dir.ScriptMaps)
for _map in map(str, vd_params.ScriptMaps):
if _map in installed_maps:
installed_maps.remove(_map)
target_dir.ScriptMaps = installed_maps
target_dir.SetInfo()
def Uninstall(params, options):
_CallHook(params, "PreRemove", options)
DeleteExtensionFileRecords(params, options)
for vd in params.VirtualDirs:
_CallHook(vd, "PreRemove", options)
RemoveDirectory(vd, options)
if vd.is_root():
# if this is installed to the root virtual directory, we can't delete it
# so remove the script maps.
RemoveScriptMaps(vd, options)
_CallHook(vd, "PostRemove", options)
for filter_def in params.Filters:
DeleteISAPIFilter(filter_def, options)
_CallHook(params, "PostRemove", options)
# Patch up any missing module names in the params, replacing them with
# the DLL name that hosts this extension/filter.
def _PatchParamsModule(params, dll_name, file_must_exist = True):
if file_must_exist:
if not os.path.isfile(dll_name):
raise ConfigurationError("%s does not exist" % (dll_name,))
# Patch up all references to the DLL.
for f in params.Filters:
if f.Path is None: f.Path = dll_name
for d in params.VirtualDirs:
for sm in d.ScriptMaps:
if sm.Module is None: sm.Module = dll_name
def GetLoaderModuleName(mod_name, check_module = None):
# find the name of the DLL hosting us.
# By default, this is "_{module_base_name}.dll"
if hasattr(sys, "frozen"):
# What to do? The .dll knows its name, but this is likely to be
# executed via a .exe, which does not know.
base, ext = os.path.splitext(mod_name)
path, base = os.path.split(base)
# handle the common case of 'foo.exe'/'foow.exe'
if base.endswith('w'):
base = base[:-1]
# For py2exe, we have '_foo.dll' as the standard pyisapi loader - but
# 'foo.dll' is what we use (it just delegates).
# So no leading '_' on the installed name.
dll_name = os.path.abspath(os.path.join(path, base + ".dll"))
else:
base, ext = os.path.splitext(mod_name)
path, base = os.path.split(base)
dll_name = os.path.abspath(os.path.join(path, "_" + base + ".dll"))
# Check we actually have it.
if check_module is None: check_module = not hasattr(sys, "frozen")
if check_module:
CheckLoaderModule(dll_name)
return dll_name
# Note the 'log' params to these 'builtin' args - old versions of pywin32
# didn't log at all in this function (by intent; anyone calling this was
# responsible). So existing code that calls this function with the old
# signature (ie, without a 'log' param) still gets the same behaviour as
# before...
def InstallModule(conf_module_name, params, options, log=lambda *args:None):
"Install the extension"
if not hasattr(sys, "frozen"):
conf_module_name = os.path.abspath(conf_module_name)
if not os.path.isfile(conf_module_name):
raise ConfigurationError("%s does not exist" % (conf_module_name,))
loader_dll = GetLoaderModuleName(conf_module_name)
_PatchParamsModule(params, loader_dll)
Install(params, options)
log(1, "Installation complete.")
def UninstallModule(conf_module_name, params, options, log=lambda *args:None):
"Remove the extension"
loader_dll = GetLoaderModuleName(conf_module_name, False)
_PatchParamsModule(params, loader_dll, False)
Uninstall(params, options)
log(1, "Uninstallation complete.")
standard_arguments = {
"install" : InstallModule,
"remove" : UninstallModule,
}
def build_usage(handler_map):
docstrings = [handler.__doc__ for handler in handler_map.values()]
all_args = dict(zip(iter(handler_map.keys()), docstrings))
arg_names = "|".join(iter(all_args.keys()))
usage_string = "%prog [options] [" + arg_names + "]\n"
usage_string += "commands:\n"
for arg, desc in all_args.items():
usage_string += " %-10s: %s" % (arg, desc) + "\n"
return usage_string[:-1]
def MergeStandardOptions(options, params):
"""
Take an options object generated by the command line and merge
the values into the IISParameters object.
"""
pass
# We support 2 ways of extending our command-line/install support.
# * Many of the installation items allow you to specify "PreInstall",
# "PostInstall", "PreRemove" and "PostRemove" hooks
# All hooks are called with the 'params' object being operated on, and
# the 'optparser' options for this session (ie, the command-line options)
# PostInstall for VirtualDirectories and Filters both have an additional
# param - the ADSI object just created.
# * You can pass your own option parser for us to use, and/or define a map
# with your own custom arg handlers. It is a map of 'arg'->function.
# The function is called with (options, log_fn, arg). The function's
# docstring is used in the usage output.
def HandleCommandLine(params, argv=None, conf_module_name = None,
default_arg = "install",
opt_parser = None, custom_arg_handlers = {}):
"""Perform installation or removal of an ISAPI filter or extension.
This module handles standard command-line options and configuration
information, and installs, removes or updates the configuration of an
ISAPI filter or extension.
You must pass your configuration information in params - all other
arguments are optional, and allow you to configure the installation
process.
"""
global verbose
from optparse import OptionParser
argv = argv or sys.argv
if not conf_module_name:
conf_module_name = sys.argv[0]
# convert to a long name so that if we were somehow registered with
# the "short" version but unregistered with the "long" version we
# still work (that will depend on exactly how the installer was
# started)
try:
conf_module_name = win32api.GetLongPathName(conf_module_name)
except win32api.error as exc:
log(2, "Couldn't determine the long name for %r: %s" %
(conf_module_name, exc))
if opt_parser is None:
# Build our own parser.
parser = OptionParser(usage='')
else:
# The caller is providing their own filter, presumably with their
# own options all setup.
parser = opt_parser
# build a usage string if we don't have one.
if not parser.get_usage():
all_handlers = standard_arguments.copy()
all_handlers.update(custom_arg_handlers)
parser.set_usage(build_usage(all_handlers))
# allow the user to use uninstall as a synonym for remove if it wasn't
# defined by the custom arg handlers.
all_handlers.setdefault('uninstall', all_handlers['remove'])
parser.add_option("-q", "--quiet",
action="store_false", dest="verbose", default=True,
help="don't print status messages to stdout")
parser.add_option("-v", "--verbosity", action="count",
dest="verbose", default=1,
help="increase the verbosity of status messages")
parser.add_option("", "--server", action="store",
help="Specifies the IIS server to install/uninstall on." \
" Default is '%s/1'" % (_IIS_OBJECT,))
(options, args) = parser.parse_args(argv[1:])
MergeStandardOptions(options, params)
verbose = options.verbose
if not args:
args = [default_arg]
try:
for arg in args:
handler = all_handlers[arg]
handler(conf_module_name, params, options, log)
except (ItemNotFound, InstallationError) as details:
if options.verbose > 1:
traceback.print_exc()
print("%s: %s" % (details.__class__.__name__, details))
except KeyError:
parser.error("Invalid arg '%s'" % arg)

View file

@ -0,0 +1,120 @@
"""Constants needed by ISAPI filters and extensions."""
# ======================================================================
# Copyright 2002-2003 by Blackdog Software Pty Ltd.
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of
# Blackdog Software not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# BLACKDOG SOFTWARE DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL BLACKDOG SOFTWARE BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
# HTTP reply codes
HTTP_CONTINUE = 100
HTTP_SWITCHING_PROTOCOLS = 101
HTTP_PROCESSING = 102
HTTP_OK = 200
HTTP_CREATED = 201
HTTP_ACCEPTED = 202
HTTP_NON_AUTHORITATIVE = 203
HTTP_NO_CONTENT = 204
HTTP_RESET_CONTENT = 205
HTTP_PARTIAL_CONTENT = 206
HTTP_MULTI_STATUS = 207
HTTP_MULTIPLE_CHOICES = 300
HTTP_MOVED_PERMANENTLY = 301
HTTP_MOVED_TEMPORARILY = 302
HTTP_SEE_OTHER = 303
HTTP_NOT_MODIFIED = 304
HTTP_USE_PROXY = 305
HTTP_TEMPORARY_REDIRECT = 307
HTTP_BAD_REQUEST = 400
HTTP_UNAUTHORIZED = 401
HTTP_PAYMENT_REQUIRED = 402
HTTP_FORBIDDEN = 403
HTTP_NOT_FOUND = 404
HTTP_METHOD_NOT_ALLOWED = 405
HTTP_NOT_ACCEPTABLE = 406
HTTP_PROXY_AUTHENTICATION_REQUIRED= 407
HTTP_REQUEST_TIME_OUT = 408
HTTP_CONFLICT = 409
HTTP_GONE = 410
HTTP_LENGTH_REQUIRED = 411
HTTP_PRECONDITION_FAILED = 412
HTTP_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_REQUEST_URI_TOO_LARGE = 414
HTTP_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_RANGE_NOT_SATISFIABLE = 416
HTTP_EXPECTATION_FAILED = 417
HTTP_UNPROCESSABLE_ENTITY = 422
HTTP_INTERNAL_SERVER_ERROR = 500
HTTP_NOT_IMPLEMENTED = 501
HTTP_BAD_GATEWAY = 502
HTTP_SERVICE_UNAVAILABLE = 503
HTTP_GATEWAY_TIME_OUT = 504
HTTP_VERSION_NOT_SUPPORTED = 505
HTTP_VARIANT_ALSO_VARIES = 506
HSE_STATUS_SUCCESS = 1
HSE_STATUS_SUCCESS_AND_KEEP_CONN = 2
HSE_STATUS_PENDING = 3
HSE_STATUS_ERROR = 4
SF_NOTIFY_SECURE_PORT = 0x00000001
SF_NOTIFY_NONSECURE_PORT = 0x00000002
SF_NOTIFY_READ_RAW_DATA = 0x00008000
SF_NOTIFY_PREPROC_HEADERS = 0x00004000
SF_NOTIFY_AUTHENTICATION = 0x00002000
SF_NOTIFY_URL_MAP = 0x00001000
SF_NOTIFY_ACCESS_DENIED = 0x00000800
SF_NOTIFY_SEND_RESPONSE = 0x00000040
SF_NOTIFY_SEND_RAW_DATA = 0x00000400
SF_NOTIFY_LOG = 0x00000200
SF_NOTIFY_END_OF_REQUEST = 0x00000080
SF_NOTIFY_END_OF_NET_SESSION = 0x00000100
SF_NOTIFY_ORDER_HIGH = 0x00080000
SF_NOTIFY_ORDER_MEDIUM = 0x00040000
SF_NOTIFY_ORDER_LOW = 0x00020000
SF_NOTIFY_ORDER_DEFAULT = SF_NOTIFY_ORDER_LOW
SF_NOTIFY_ORDER_MASK = (SF_NOTIFY_ORDER_HIGH | \
SF_NOTIFY_ORDER_MEDIUM | \
SF_NOTIFY_ORDER_LOW)
SF_STATUS_REQ_FINISHED = 134217728 # 0x8000000
SF_STATUS_REQ_FINISHED_KEEP_CONN = 134217728 + 1
SF_STATUS_REQ_NEXT_NOTIFICATION = 134217728 + 2
SF_STATUS_REQ_HANDLED_NOTIFICATION = 134217728 + 3
SF_STATUS_REQ_ERROR = 134217728 + 4
SF_STATUS_REQ_READ_NEXT = 134217728 + 5
HSE_IO_SYNC = 0x00000001 # for WriteClient
HSE_IO_ASYNC = 0x00000002 # for WriteClient/TF/EU
HSE_IO_DISCONNECT_AFTER_SEND = 0x00000004 # for TF
HSE_IO_SEND_HEADERS = 0x00000008 # for TF
HSE_IO_NODELAY = 0x00001000 # turn off nagling
# These two are only used by VectorSend
HSE_IO_FINAL_SEND = 0x00000010
HSE_IO_CACHE_RESPONSE = 0x00000020
HSE_EXEC_URL_NO_HEADERS = 0x02
HSE_EXEC_URL_IGNORE_CURRENT_INTERCEPTOR = 0x04
HSE_EXEC_URL_IGNORE_VALIDATION_AND_RANGE = 0x10
HSE_EXEC_URL_DISABLE_CUSTOM_ERROR = 0x20
HSE_EXEC_URL_SSI_CMD = 0x40
HSE_EXEC_URL_HTTP_CACHE_ELIGIBLE = 0x80

View file

@ -0,0 +1,20 @@
In this directory you will find examples of ISAPI filters and extensions.
The filter loading mechanism works like this:
* IIS loads the special Python "loader" DLL. This DLL will generally have a
leading underscore as part of its name.
* This loader DLL looks for a Python module, by removing the first letter of
the DLL base name.
This means that an ISAPI extension module consists of 2 key files - the loader
DLL (eg, "_MyIISModule.dll", and a Python module (which for this example
would be "MyIISModule.py")
When you install an ISAPI extension, the installation code checks to see if
there is a loader DLL for your implementation file - if one does not exist,
or the standard loader is different, it is copied and renamed accordingly.
We use this mechanism to provide the maximum separation between different
Python extensions installed on the same server - otherwise filter order and
other tricky IIS semantics would need to be replicated. Also, each filter
gets its own thread-pool, etc.

View file

@ -0,0 +1,196 @@
# This extension demonstrates some advanced features of the Python ISAPI
# framework.
# We demonstrate:
# * Reloading your Python module without shutting down IIS (eg, when your
# .py implementation file changes.)
# * Custom command-line handling - both additional options and commands.
# * Using a query string - any part of the URL after a '?' is assumed to
# be "variable names" separated by '&' - we will print the values of
# these server variables.
# * If the tail portion of the URL is "ReportUnhealthy", IIS will be
# notified we are unhealthy via a HSE_REQ_REPORT_UNHEALTHY request.
# Whether this is acted upon depends on if the IIS health-checking
# tools are installed, but you should always see the reason written
# to the Windows event log - see the IIS documentation for more.
from isapi import isapicon
from isapi.simple import SimpleExtension
import sys, os, stat
if hasattr(sys, "isapidllhandle"):
import win32traceutil
# Notes on reloading
# If your HttpFilterProc or HttpExtensionProc functions raises
# 'isapi.InternalReloadException', the framework will not treat it
# as an error but instead will terminate your extension, reload your
# extension module, re-initialize the instance, and re-issue the request.
# The Initialize functions are called with None as their param. The
# return code from the terminate function is ignored.
#
# This is all the framework does to help you. It is up to your code
# when you raise this exception. This sample uses a Win32 "find
# notification". Whenever windows tells us one of the files in the
# directory has changed, we check if the time of our source-file has
# changed, and set a flag. Next imcoming request, we check the flag and
# raise the special exception if set.
#
# The end result is that the module is automatically reloaded whenever
# the source-file changes - you need take no further action to see your
# changes reflected in the running server.
# The framework only reloads your module - if you have libraries you
# depend on and also want reloaded, you must arrange for this yourself.
# One way of doing this would be to special case the import of these
# modules. Eg:
# --
# try:
# my_module = reload(my_module) # module already imported - reload it
# except NameError:
# import my_module # first time around - import it.
# --
# When your module is imported for the first time, the NameError will
# be raised, and the module imported. When the ISAPI framework reloads
# your module, the existing module will avoid the NameError, and allow
# you to reload that module.
from isapi import InternalReloadException
import win32event, win32file, winerror, win32con, threading
try:
reload_counter += 1
except NameError:
reload_counter = 0
# A watcher thread that checks for __file__ changing.
# When it detects it, it simply sets "change_detected" to true.
class ReloadWatcherThread(threading.Thread):
def __init__(self):
self.change_detected = False
self.filename = __file__
if self.filename.endswith("c") or self.filename.endswith("o"):
self.filename = self.filename[:-1]
self.handle = win32file.FindFirstChangeNotification(
os.path.dirname(self.filename),
False, # watch tree?
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE)
threading.Thread.__init__(self)
def run(self):
last_time = os.stat(self.filename)[stat.ST_MTIME]
while 1:
try:
rc = win32event.WaitForSingleObject(self.handle,
win32event.INFINITE)
win32file.FindNextChangeNotification(self.handle)
except win32event.error as details:
# handle closed - thread should terminate.
if details.winerror != winerror.ERROR_INVALID_HANDLE:
raise
break
this_time = os.stat(self.filename)[stat.ST_MTIME]
if this_time != last_time:
print("Detected file change - flagging for reload.")
self.change_detected = True
last_time = this_time
def stop(self):
win32file.FindCloseChangeNotification(self.handle)
# The ISAPI extension - handles requests in our virtual dir, and sends the
# response to the client.
class Extension(SimpleExtension):
"Python advanced sample Extension"
def __init__(self):
self.reload_watcher = ReloadWatcherThread()
self.reload_watcher.start()
def HttpExtensionProc(self, ecb):
# NOTE: If you use a ThreadPoolExtension, you must still perform
# this check in HttpExtensionProc - raising the exception from
# The "Dispatch" method will just cause the exception to be
# rendered to the browser.
if self.reload_watcher.change_detected:
print("Doing reload")
raise InternalReloadException
url = ecb.GetServerVariable("UNICODE_URL")
if url.endswith("ReportUnhealthy"):
ecb.ReportUnhealthy("I'm a little sick")
ecb.SendResponseHeaders("200 OK", "Content-Type: text/html\r\n\r\n", 0)
print("<HTML><BODY>", file=ecb)
qs = ecb.GetServerVariable("QUERY_STRING")
if qs:
queries = qs.split("&")
print("<PRE>", file=ecb)
for q in queries:
val = ecb.GetServerVariable(q, '&lt;no such variable&gt;')
print("%s=%r" % (q, val), file=ecb)
print("</PRE><P/>", file=ecb)
print("This module has been imported", file=ecb)
print("%d times" % (reload_counter,), file=ecb)
print("</BODY></HTML>", file=ecb)
ecb.close()
return isapicon.HSE_STATUS_SUCCESS
def TerminateExtension(self, status):
self.reload_watcher.stop()
# The entry points for the ISAPI extension.
def __ExtensionFactory__():
return Extension()
# Our special command line customization.
# Pre-install hook for our virtual directory.
def PreInstallDirectory(params, options):
# If the user used our special '--description' option,
# then we override our default.
if options.description:
params.Description = options.description
# Post install hook for our entire script
def PostInstall(params, options):
print()
print("The sample has been installed.")
print("Point your browser to /AdvancedPythonSample")
print("If you modify the source file and reload the page,")
print("you should see the reload counter increment")
# Handler for our custom 'status' argument.
def status_handler(options, log, arg):
"Query the status of something"
print("Everything seems to be fine!")
custom_arg_handlers = {"status": status_handler}
if __name__=='__main__':
# If run from the command-line, install ourselves.
from isapi.install import *
params = ISAPIParameters(PostInstall = PostInstall)
# Setup the virtual directories - this is a list of directories our
# extension uses - in this case only 1.
# Each extension has a "script map" - this is the mapping of ISAPI
# extensions.
sm = [
ScriptMapParams(Extension="*", Flags=0)
]
vd = VirtualDirParameters(Name="AdvancedPythonSample",
Description = Extension.__doc__,
ScriptMaps = sm,
ScriptMapUpdate = "replace",
# specify the pre-install hook.
PreInstall = PreInstallDirectory
)
params.VirtualDirs = [vd]
# Setup our custom option parser.
from optparse import OptionParser
parser = OptionParser('') # blank usage, so isapi sets it.
parser.add_option("", "--description",
action="store",
help="custom description to use for the virtual directory")
HandleCommandLine(params, opt_parser=parser,
custom_arg_handlers = custom_arg_handlers)

View file

@ -0,0 +1,109 @@
# This is a sample ISAPI extension written in Python.
#
# Please see README.txt in this directory, and specifically the
# information about the "loader" DLL - installing this sample will create
# "_redirector.dll" in the current directory. The readme explains this.
# Executing this script (or any server config script) will install the extension
# into your web server. As the server executes, the PyISAPI framework will load
# this module and create your Extension and Filter objects.
# This is the simplest possible redirector (or proxy) we can write. The
# extension installs with a mask of '*' in the root of the site.
# As an added bonus though, we optionally show how, on IIS6 and later, we
# can use HSE_ERQ_EXEC_URL to ignore certain requests - in IIS5 and earlier
# we can only do this with an ISAPI filter - see redirector_with_filter for
# an example. If this sample is run on IIS5 or earlier it simply ignores
# any excludes.
from isapi import isapicon, threaded_extension
import sys
import traceback
try:
from urllib.request import urlopen
except ImportError:
# py3k spelling...
from urllib.request import urlopen
import win32api
# sys.isapidllhandle will exist when we are loaded by the IIS framework.
# In this case we redirect our output to the win32traceutil collector.
if hasattr(sys, "isapidllhandle"):
import win32traceutil
# The site we are proxying.
proxy = "http://www.python.org"
# Urls we exclude (ie, allow IIS to handle itself) - all are lowered,
# and these entries exist by default on Vista...
excludes = ["/iisstart.htm", "/welcome.png"]
# An "io completion" function, called when ecb.ExecURL completes...
def io_callback(ecb, url, cbIO, errcode):
# Get the status of our ExecURL
httpstatus, substatus, win32 = ecb.GetExecURLStatus()
print("ExecURL of %r finished with http status %d.%d, win32 status %d (%s)" % (
url, httpstatus, substatus, win32, win32api.FormatMessage(win32).strip()))
# nothing more to do!
ecb.DoneWithSession()
# The ISAPI extension - handles all requests in the site.
class Extension(threaded_extension.ThreadPoolExtension):
"Python sample Extension"
def Dispatch(self, ecb):
# Note that our ThreadPoolExtension base class will catch exceptions
# in our Dispatch method, and write the traceback to the client.
# That is perfect for this sample, so we don't catch our own.
#print 'IIS dispatching "%s"' % (ecb.GetServerVariable("URL"),)
url = ecb.GetServerVariable("URL").decode("ascii")
for exclude in excludes:
if url.lower().startswith(exclude):
print("excluding %s" % url)
if ecb.Version < 0x60000:
print("(but this is IIS5 or earlier - can't do 'excludes')")
else:
ecb.IOCompletion(io_callback, url)
ecb.ExecURL(None, None, None, None, None, isapicon.HSE_EXEC_URL_IGNORE_CURRENT_INTERCEPTOR)
return isapicon.HSE_STATUS_PENDING
new_url = proxy + url
print("Opening %s" % new_url)
fp = urlopen(new_url)
headers = fp.info()
# subtle py3k breakage: in py3k, str(headers) has normalized \r\n
# back to \n and also stuck an extra \n term. py2k leaves the
# \r\n from the server in tact and finishes with a single term.
if sys.version_info < (3,0):
header_text = str(headers) + "\r\n"
else:
# take *all* trailing \n off, replace remaining with
# \r\n, then add the 2 trailing \r\n.
header_text = str(headers).rstrip('\n').replace('\n', '\r\n') + '\r\n\r\n'
ecb.SendResponseHeaders("200 OK", header_text, False)
ecb.WriteClient(fp.read())
ecb.DoneWithSession()
print("Returned data from '%s'" % (new_url,))
return isapicon.HSE_STATUS_SUCCESS
# The entry points for the ISAPI extension.
def __ExtensionFactory__():
return Extension()
if __name__=='__main__':
# If run from the command-line, install ourselves.
from isapi.install import *
params = ISAPIParameters()
# Setup the virtual directories - this is a list of directories our
# extension uses - in this case only 1.
# Each extension has a "script map" - this is the mapping of ISAPI
# extensions.
sm = [
ScriptMapParams(Extension="*", Flags=0)
]
vd = VirtualDirParameters(Name="/",
Description = Extension.__doc__,
ScriptMaps = sm,
ScriptMapUpdate = "replace"
)
params.VirtualDirs = [vd]
HandleCommandLine(params)

View file

@ -0,0 +1,78 @@
# This is a sample ISAPI extension written in Python.
# This is like the other 'redirector' samples, but uses asnch IO when writing
# back to the client (it does *not* use asynch io talking to the remote
# server!)
from isapi import isapicon, threaded_extension
import sys
import traceback
import urllib.request, urllib.parse, urllib.error
# sys.isapidllhandle will exist when we are loaded by the IIS framework.
# In this case we redirect our output to the win32traceutil collector.
if hasattr(sys, "isapidllhandle"):
import win32traceutil
# The site we are proxying.
proxy = "http://www.python.org"
# We synchronously read chunks of this size then asynchronously write them.
CHUNK_SIZE=8192
# The callback made when IIS completes the asynch write.
def io_callback(ecb, fp, cbIO, errcode):
print("IO callback", ecb, fp, cbIO, errcode)
chunk = fp.read(CHUNK_SIZE)
if chunk:
ecb.WriteClient(chunk, isapicon.HSE_IO_ASYNC)
# and wait for the next callback to say this chunk is done.
else:
# eof - say we are complete.
fp.close()
ecb.DoneWithSession()
# The ISAPI extension - handles all requests in the site.
class Extension(threaded_extension.ThreadPoolExtension):
"Python sample proxy server - asynch version."
def Dispatch(self, ecb):
print('IIS dispatching "%s"' % (ecb.GetServerVariable("URL"),))
url = ecb.GetServerVariable("URL")
new_url = proxy + url
print("Opening %s" % new_url)
fp = urllib.request.urlopen(new_url)
headers = fp.info()
ecb.SendResponseHeaders("200 OK", str(headers) + "\r\n", False)
# now send the first chunk asynchronously
ecb.ReqIOCompletion(io_callback, fp)
chunk = fp.read(CHUNK_SIZE)
if chunk:
ecb.WriteClient(chunk, isapicon.HSE_IO_ASYNC)
return isapicon.HSE_STATUS_PENDING
# no data - just close things now.
ecb.DoneWithSession()
return isapicon.HSE_STATUS_SUCCESS
# The entry points for the ISAPI extension.
def __ExtensionFactory__():
return Extension()
if __name__=='__main__':
# If run from the command-line, install ourselves.
from isapi.install import *
params = ISAPIParameters()
# Setup the virtual directories - this is a list of directories our
# extension uses - in this case only 1.
# Each extension has a "script map" - this is the mapping of ISAPI
# extensions.
sm = [
ScriptMapParams(Extension="*", Flags=0)
]
vd = VirtualDirParameters(Name="/",
Description = Extension.__doc__,
ScriptMaps = sm,
ScriptMapUpdate = "replace"
)
params.VirtualDirs = [vd]
HandleCommandLine(params)

View file

@ -0,0 +1,155 @@
# This is a sample configuration file for an ISAPI filter and extension
# written in Python.
#
# Please see README.txt in this directory, and specifically the
# information about the "loader" DLL - installing this sample will create
# "_redirector_with_filter.dll" in the current directory. The readme explains
# this.
# Executing this script (or any server config script) will install the extension
# into your web server. As the server executes, the PyISAPI framework will load
# this module and create your Extension and Filter objects.
# This sample provides sample redirector:
# It is implemented by a filter and an extension, so that some requests can
# be ignored. Compare with 'redirector_simple' which avoids the filter, but
# is unable to selectively ignore certain requests.
# The process is sample uses is:
# * The filter is installed globally, as all filters are.
# * A Virtual Directory named "python" is setup. This dir has our ISAPI
# extension as the only application, mapped to file-extension '*'. Thus, our
# extension handles *all* requests in this directory.
# The basic process is that the filter does URL rewriting, redirecting every
# URL to our Virtual Directory. Our extension then handles this request,
# forwarding the data from the proxied site.
# For example:
# * URL of "index.html" comes in.
# * Filter rewrites this to "/python/index.html"
# * Our extension sees the full "/python/index.html", removes the leading
# portion, and opens and forwards the remote URL.
# This sample is very small - it avoid most error handling, etc. It is for
# demonstration purposes only.
from isapi import isapicon, threaded_extension
from isapi.simple import SimpleFilter
import sys
import traceback
import urllib.request, urllib.parse, urllib.error
# sys.isapidllhandle will exist when we are loaded by the IIS framework.
# In this case we redirect our output to the win32traceutil collector.
if hasattr(sys, "isapidllhandle"):
import win32traceutil
# The site we are proxying.
proxy = "http://www.python.org"
# The name of the virtual directory we install in, and redirect from.
virtualdir = "/python"
# The key feature of this redirector over the simple redirector is that it
# can choose to ignore certain responses by having the filter not rewrite them
# to our virtual dir. For this sample, we just exclude the IIS help directory.
# The ISAPI extension - handles requests in our virtual dir, and sends the
# response to the client.
class Extension(threaded_extension.ThreadPoolExtension):
"Python sample Extension"
def Dispatch(self, ecb):
# Note that our ThreadPoolExtension base class will catch exceptions
# in our Dispatch method, and write the traceback to the client.
# That is perfect for this sample, so we don't catch our own.
#print 'IIS dispatching "%s"' % (ecb.GetServerVariable("URL"),)
url = ecb.GetServerVariable("URL")
if url.startswith(virtualdir):
new_url = proxy + url[len(virtualdir):]
print("Opening", new_url)
fp = urllib.request.urlopen(new_url)
headers = fp.info()
ecb.SendResponseHeaders("200 OK", str(headers) + "\r\n", False)
ecb.WriteClient(fp.read())
ecb.DoneWithSession()
print("Returned data from '%s'!" % (new_url,))
else:
# this should never happen - we should only see requests that
# start with our virtual directory name.
print("Not proxying '%s'" % (url,))
# The ISAPI filter.
class Filter(SimpleFilter):
"Sample Python Redirector"
filter_flags = isapicon.SF_NOTIFY_PREPROC_HEADERS | \
isapicon.SF_NOTIFY_ORDER_DEFAULT
def HttpFilterProc(self, fc):
#print "Filter Dispatch"
nt = fc.NotificationType
if nt != isapicon.SF_NOTIFY_PREPROC_HEADERS:
return isapicon.SF_STATUS_REQ_NEXT_NOTIFICATION
pp = fc.GetData()
url = pp.GetHeader("url")
#print "URL is '%s'" % (url,)
prefix = virtualdir
if not url.startswith(prefix):
new_url = prefix + url
print("New proxied URL is '%s'" % (new_url,))
pp.SetHeader("url", new_url)
# For the sake of demonstration, show how the FilterContext
# attribute is used. It always starts out life as None, and
# any assignments made are automatically decref'd by the
# framework during a SF_NOTIFY_END_OF_NET_SESSION notification.
if fc.FilterContext is None:
fc.FilterContext = 0
fc.FilterContext += 1
print("This is request number %d on this connection" % fc.FilterContext)
return isapicon.SF_STATUS_REQ_HANDLED_NOTIFICATION
else:
print("Filter ignoring URL '%s'" % (url,))
# Some older code that handled SF_NOTIFY_URL_MAP.
#~ print "Have URL_MAP notify"
#~ urlmap = fc.GetData()
#~ print "URI is", urlmap.URL
#~ print "Path is", urlmap.PhysicalPath
#~ if urlmap.URL.startswith("/UC/"):
#~ # Find the /UC/ in the physical path, and nuke it (except
#~ # as the path is physical, it is \)
#~ p = urlmap.PhysicalPath
#~ pos = p.index("\\UC\\")
#~ p = p[:pos] + p[pos+3:]
#~ p = r"E:\src\pyisapi\webroot\PyTest\formTest.htm"
#~ print "New path is", p
#~ urlmap.PhysicalPath = p
# The entry points for the ISAPI extension.
def __FilterFactory__():
return Filter()
def __ExtensionFactory__():
return Extension()
if __name__=='__main__':
# If run from the command-line, install ourselves.
from isapi.install import *
params = ISAPIParameters()
# Setup all filters - these are global to the site.
params.Filters = [
FilterParameters(Name="PythonRedirector",
Description=Filter.__doc__),
]
# Setup the virtual directories - this is a list of directories our
# extension uses - in this case only 1.
# Each extension has a "script map" - this is the mapping of ISAPI
# extensions.
sm = [
ScriptMapParams(Extension="*", Flags=0)
]
vd = VirtualDirParameters(Name=virtualdir[1:],
Description = Extension.__doc__,
ScriptMaps = sm,
ScriptMapUpdate = "replace"
)
params.VirtualDirs = [vd]
HandleCommandLine(params)

View file

@ -0,0 +1,154 @@
# This extension is used mainly for testing purposes - it is not
# designed to be a simple sample, but instead is a hotch-potch of things
# that attempts to exercise the framework.
from isapi import isapicon
from isapi.simple import SimpleExtension
import sys, os, stat
if hasattr(sys, "isapidllhandle"):
import win32traceutil
# We use the same reload support as 'advanced.py' demonstrates.
from isapi import InternalReloadException
import win32event, win32file, winerror, win32con, threading
# A watcher thread that checks for __file__ changing.
# When it detects it, it simply sets "change_detected" to true.
class ReloadWatcherThread(threading.Thread):
def __init__(self):
self.change_detected = False
self.filename = __file__
if self.filename.endswith("c") or self.filename.endswith("o"):
self.filename = self.filename[:-1]
self.handle = win32file.FindFirstChangeNotification(
os.path.dirname(self.filename),
False, # watch tree?
win32con.FILE_NOTIFY_CHANGE_LAST_WRITE)
threading.Thread.__init__(self)
def run(self):
last_time = os.stat(self.filename)[stat.ST_MTIME]
while 1:
try:
rc = win32event.WaitForSingleObject(self.handle,
win32event.INFINITE)
win32file.FindNextChangeNotification(self.handle)
except win32event.error as details:
# handle closed - thread should terminate.
if details.winerror != winerror.ERROR_INVALID_HANDLE:
raise
break
this_time = os.stat(self.filename)[stat.ST_MTIME]
if this_time != last_time:
print("Detected file change - flagging for reload.")
self.change_detected = True
last_time = this_time
def stop(self):
win32file.FindCloseChangeNotification(self.handle)
def TransmitFileCallback(ecb, hFile, cbIO, errCode):
print("Transmit complete!")
ecb.close()
# The ISAPI extension - handles requests in our virtual dir, and sends the
# response to the client.
class Extension(SimpleExtension):
"Python test Extension"
def __init__(self):
self.reload_watcher = ReloadWatcherThread()
self.reload_watcher.start()
def HttpExtensionProc(self, ecb):
# NOTE: If you use a ThreadPoolExtension, you must still perform
# this check in HttpExtensionProc - raising the exception from
# The "Dispatch" method will just cause the exception to be
# rendered to the browser.
if self.reload_watcher.change_detected:
print("Doing reload")
raise InternalReloadException
if ecb.GetServerVariable("UNICODE_URL").endswith("test.py"):
file_flags = win32con.FILE_FLAG_SEQUENTIAL_SCAN | win32con.FILE_FLAG_OVERLAPPED
hfile = win32file.CreateFile(__file__, win32con.GENERIC_READ,
0, None, win32con.OPEN_EXISTING,
file_flags, None)
flags = isapicon.HSE_IO_ASYNC | isapicon.HSE_IO_DISCONNECT_AFTER_SEND | \
isapicon.HSE_IO_SEND_HEADERS
# We pass hFile to the callback simply as a way of keeping it alive
# for the duration of the transmission
try:
ecb.TransmitFile(TransmitFileCallback, hfile,
int(hfile),
"200 OK",
0, 0, None, None, flags)
except:
# Errors keep this source file open!
hfile.Close()
raise
else:
# default response
ecb.SendResponseHeaders("200 OK", "Content-Type: text/html\r\n\r\n", 0)
print("<HTML><BODY>", file=ecb)
print("The root of this site is at", ecb.MapURLToPath("/"), file=ecb)
print("</BODY></HTML>", file=ecb)
ecb.close()
return isapicon.HSE_STATUS_SUCCESS
def TerminateExtension(self, status):
self.reload_watcher.stop()
# The entry points for the ISAPI extension.
def __ExtensionFactory__():
return Extension()
# Our special command line customization.
# Pre-install hook for our virtual directory.
def PreInstallDirectory(params, options):
# If the user used our special '--description' option,
# then we override our default.
if options.description:
params.Description = options.description
# Post install hook for our entire script
def PostInstall(params, options):
print()
print("The sample has been installed.")
print("Point your browser to /PyISAPITest")
# Handler for our custom 'status' argument.
def status_handler(options, log, arg):
"Query the status of something"
print("Everything seems to be fine!")
custom_arg_handlers = {"status": status_handler}
if __name__=='__main__':
# If run from the command-line, install ourselves.
from isapi.install import *
params = ISAPIParameters(PostInstall = PostInstall)
# Setup the virtual directories - this is a list of directories our
# extension uses - in this case only 1.
# Each extension has a "script map" - this is the mapping of ISAPI
# extensions.
sm = [
ScriptMapParams(Extension="*", Flags=0)
]
vd = VirtualDirParameters(Name="PyISAPITest",
Description = Extension.__doc__,
ScriptMaps = sm,
ScriptMapUpdate = "replace",
# specify the pre-install hook.
PreInstall = PreInstallDirectory
)
params.VirtualDirs = [vd]
# Setup our custom option parser.
from optparse import OptionParser
parser = OptionParser('') # blank usage, so isapi sets it.
parser.add_option("", "--description",
action="store",
help="custom description to use for the virtual directory")
HandleCommandLine(params, opt_parser=parser,
custom_arg_handlers = custom_arg_handlers)

View file

@ -0,0 +1,68 @@
"""Simple base-classes for extensions and filters.
None of the filter and extension functions are considered 'optional' by the
framework. These base-classes provide simple implementations for the
Initialize and Terminate functions, allowing you to omit them,
It is not necessary to use these base-classes - but if you don't, you
must ensure each of the required methods are implemented.
"""
class SimpleExtension:
"Base class for a simple ISAPI extension"
def __init__(self):
pass
def GetExtensionVersion(self, vi):
"""Called by the ISAPI framework to get the extension version
The default implementation uses the classes docstring to
set the extension description."""
# nod to our reload capability - vi is None when we are reloaded.
if vi is not None:
vi.ExtensionDesc = self.__doc__
def HttpExtensionProc(self, control_block):
"""Called by the ISAPI framework for each extension request.
sub-classes must provide an implementation for this method.
"""
raise NotImplementedError("sub-classes should override HttpExtensionProc")
def TerminateExtension(self, status):
"""Called by the ISAPI framework as the extension terminates.
"""
pass
class SimpleFilter:
"Base class for a a simple ISAPI filter"
filter_flags = None
def __init__(self):
pass
def GetFilterVersion(self, fv):
"""Called by the ISAPI framework to get the extension version
The default implementation uses the classes docstring to
set the extension description, and uses the classes
filter_flags attribute to set the ISAPI filter flags - you
must specify filter_flags in your class.
"""
if self.filter_flags is None:
raise RuntimeError("You must specify the filter flags")
# nod to our reload capability - fv is None when we are reloaded.
if fv is not None:
fv.Flags = self.filter_flags
fv.FilterDesc = self.__doc__
def HttpFilterProc(self, fc):
"""Called by the ISAPI framework for each filter request.
sub-classes must provide an implementation for this method.
"""
raise NotImplementedError("sub-classes should override HttpExtensionProc")
def TerminateFilter(self, status):
"""Called by the ISAPI framework as the filter terminates.
"""
pass

View file

@ -0,0 +1,3 @@
This is a directory for tests of the PyISAPI framework.
For demos, please see the pyisapi 'samples' directory.

View file

@ -0,0 +1,111 @@
# This is an ISAPI extension purely for testing purposes. It is NOT
# a 'demo' (even though it may be useful!)
#
# Install this extension, then point your browser to:
# "http://localhost/pyisapi_test/test1"
# This will execute the method 'test1' below. See below for the list of
# test methods that are acceptable.
from isapi import isapicon, threaded_extension, ExtensionError
from isapi.simple import SimpleFilter
import traceback
import urllib.request, urllib.parse, urllib.error
import winerror
# If we have no console (eg, am running from inside IIS), redirect output
# somewhere useful - in this case, the standard win32 trace collector.
import win32api
try:
win32api.GetConsoleTitle()
except win32api.error:
# No console - redirect
import win32traceutil
# The ISAPI extension - handles requests in our virtual dir, and sends the
# response to the client.
class Extension(threaded_extension.ThreadPoolExtension):
"Python ISAPI Tester"
def Dispatch(self, ecb):
print('Tester dispatching "%s"' % (ecb.GetServerVariable("URL"),))
url = ecb.GetServerVariable("URL")
test_name = url.split("/")[-1]
meth = getattr(self, test_name, None)
if meth is None:
raise AttributeError("No test named '%s'" % (test_name,))
result = meth(ecb)
if result is None:
# This means the test finalized everything
return
ecb.SendResponseHeaders("200 OK", "Content-type: text/html\r\n\r\n",
False)
print("<HTML><BODY>Finished running test <i>", test_name, "</i>", file=ecb)
print("<pre>", file=ecb)
print(result, file=ecb)
print("</pre>", file=ecb)
print("</BODY></HTML>", file=ecb)
ecb.DoneWithSession()
def test1(self, ecb):
try:
ecb.GetServerVariable("foo bar")
raise RuntimeError("should have failed!")
except ExtensionError as err:
assert err.errno == winerror.ERROR_INVALID_INDEX, err
return "worked!"
def test_long_vars(self, ecb):
qs = ecb.GetServerVariable("QUERY_STRING")
# Our implementation has a default buffer size of 8k - so we test
# the code that handles an overflow by ensuring there are more
# than 8k worth of chars in the URL.
expected_query = ('x' * 8500)
if len(qs)==0:
# Just the URL with no query part - redirect to myself, but with
# a huge query portion.
me = ecb.GetServerVariable("URL")
headers = "Location: " + me + "?" + expected_query + "\r\n\r\n"
ecb.SendResponseHeaders("301 Moved", headers)
ecb.DoneWithSession()
return None
if qs == expected_query:
return "Total length of variable is %d - test worked!" % (len(qs),)
else:
return "Unexpected query portion! Got %d chars, expected %d" % \
(len(qs), len(expected_query))
def test_unicode_vars(self, ecb):
# We need to check that we are running IIS6! This seems the only
# effective way from an extension.
ver = float(ecb.GetServerVariable("SERVER_SOFTWARE").split('/')[1])
if ver < 6.0:
return "This is IIS version %g - unicode only works in IIS6 and later" % ver
us = ecb.GetServerVariable("UNICODE_SERVER_NAME")
if not isinstance(us, str):
raise RuntimeError("unexpected type!")
if us != str(ecb.GetServerVariable("SERVER_NAME")):
raise RuntimeError("Unicode and non-unicode values were not the same")
return "worked!"
# The entry points for the ISAPI extension.
def __ExtensionFactory__():
return Extension()
if __name__=='__main__':
# If run from the command-line, install ourselves.
from isapi.install import *
params = ISAPIParameters()
# Setup the virtual directories - this is a list of directories our
# extension uses - in this case only 1.
# Each extension has a "script map" - this is the mapping of ISAPI
# extensions.
sm = [
ScriptMapParams(Extension="*", Flags=0)
]
vd = VirtualDirParameters(Name="pyisapi_test",
Description = Extension.__doc__,
ScriptMaps = sm,
ScriptMapUpdate = "replace"
)
params.VirtualDirs = [vd]
HandleCommandLine(params)

View file

@ -0,0 +1,171 @@
"""An ISAPI extension base class implemented using a thread-pool."""
# $Id$
import sys
import time
from isapi import isapicon, ExtensionError
import isapi.simple
from win32file import GetQueuedCompletionStatus, CreateIoCompletionPort, \
PostQueuedCompletionStatus, CloseHandle
from win32security import SetThreadToken
from win32event import INFINITE
from pywintypes import OVERLAPPED
import threading
import traceback
ISAPI_REQUEST = 1
ISAPI_SHUTDOWN = 2
class WorkerThread(threading.Thread):
def __init__(self, extension, io_req_port):
self.running = False
self.io_req_port = io_req_port
self.extension = extension
threading.Thread.__init__(self)
# We wait 15 seconds for a thread to terminate, but if it fails to,
# we don't want the process to hang at exit waiting for it...
self.setDaemon(True)
def run(self):
self.running = True
while self.running:
errCode, bytes, key, overlapped = \
GetQueuedCompletionStatus(self.io_req_port, INFINITE)
if key == ISAPI_SHUTDOWN and overlapped is None:
break
# Let the parent extension handle the command.
dispatcher = self.extension.dispatch_map.get(key)
if dispatcher is None:
raise RuntimeError("Bad request '%s'" % (key,))
dispatcher(errCode, bytes, key, overlapped)
def call_handler(self, cblock):
self.extension.Dispatch(cblock)
# A generic thread-pool based extension, using IO Completion Ports.
# Sub-classes can override one method to implement a simple extension, or
# may leverage the CompletionPort to queue their own requests, and implement a
# fully asynch extension.
class ThreadPoolExtension(isapi.simple.SimpleExtension):
"Base class for an ISAPI extension based around a thread-pool"
max_workers = 20
worker_shutdown_wait = 15000 # 15 seconds for workers to quit...
def __init__(self):
self.workers = []
# extensible dispatch map, for sub-classes that need to post their
# own requests to the completion port.
# Each of these functions is called with the result of
# GetQueuedCompletionStatus for our port.
self.dispatch_map = {
ISAPI_REQUEST: self.DispatchConnection,
}
def GetExtensionVersion(self, vi):
isapi.simple.SimpleExtension.GetExtensionVersion(self, vi)
# As per Q192800, the CompletionPort should be created with the number
# of processors, even if the number of worker threads is much larger.
# Passing 0 means the system picks the number.
self.io_req_port = CreateIoCompletionPort(-1, None, 0, 0)
# start up the workers
self.workers = []
for i in range(self.max_workers):
worker = WorkerThread(self, self.io_req_port)
worker.start()
self.workers.append(worker)
def HttpExtensionProc(self, control_block):
overlapped = OVERLAPPED()
overlapped.object = control_block
PostQueuedCompletionStatus(self.io_req_port, 0, ISAPI_REQUEST, overlapped)
return isapicon.HSE_STATUS_PENDING
def TerminateExtension(self, status):
for worker in self.workers:
worker.running = False
for worker in self.workers:
PostQueuedCompletionStatus(self.io_req_port, 0, ISAPI_SHUTDOWN, None)
# wait for them to terminate - pity we aren't using 'native' threads
# as then we could do a smart wait - but now we need to poll....
end_time = time.time() + self.worker_shutdown_wait/1000
alive = self.workers
while alive:
if time.time() > end_time:
# xxx - might be nice to log something here.
break
time.sleep(0.2)
alive = [w for w in alive if w.isAlive()]
self.dispatch_map = {} # break circles
CloseHandle(self.io_req_port)
# This is the one operation the base class supports - a simple
# Connection request. We setup the thread-token, and dispatch to the
# sub-class's 'Dispatch' method.
def DispatchConnection(self, errCode, bytes, key, overlapped):
control_block = overlapped.object
# setup the correct user for this request
hRequestToken = control_block.GetImpersonationToken()
SetThreadToken(None, hRequestToken)
try:
try:
self.Dispatch(control_block)
except:
self.HandleDispatchError(control_block)
finally:
# reset the security context
SetThreadToken(None, None)
def Dispatch(self, ecb):
"""Overridden by the sub-class to handle connection requests.
This class creates a thread-pool using a Windows completion port,
and dispatches requests via this port. Sub-classes can generally
implement each connection request using blocking reads and writes, and
the thread-pool will still provide decent response to the end user.
The sub-class can set a max_workers attribute (default is 20). Note
that this generally does *not* mean 20 threads will all be concurrently
running, via the magic of Windows completion ports.
There is no default implementation - sub-classes must implement this.
"""
raise NotImplementedError("sub-classes should override Dispatch")
def HandleDispatchError(self, ecb):
"""Handles errors in the Dispatch method.
When a Dispatch method call fails, this method is called to handle
the exception. The default implementation formats the traceback
in the browser.
"""
ecb.HttpStatusCode = isapicon.HSE_STATUS_ERROR
#control_block.LogData = "we failed!"
exc_typ, exc_val, exc_tb = sys.exc_info()
limit = None
try:
try:
import cgi
ecb.SendResponseHeaders("200 OK", "Content-type: text/html\r\n\r\n",
False)
print(file=ecb)
print("<H3>Traceback (most recent call last):</H3>", file=ecb)
list = traceback.format_tb(exc_tb, limit) + \
traceback.format_exception_only(exc_typ, exc_val)
print("<PRE>%s<B>%s</B></PRE>" % (
cgi.escape("".join(list[:-1])), cgi.escape(list[-1]),), file=ecb)
except ExtensionError:
# The client disconnected without reading the error body -
# its probably not a real browser at the other end, ignore it.
pass
except:
print("FAILED to render the error message!")
traceback.print_exc()
print("ORIGINAL extension error:")
traceback.print_exception(exc_typ, exc_val, exc_tb)
finally:
# holding tracebacks in a local of a frame that may itself be
# part of a traceback used to be evil and cause leaks!
exc_tb = None
ecb.DoneWithSession()