HOWTO: Add and Remove an ISAPI Filter using JScript

Question:

Hello,

I am looking to automate installation of an ISAPI filter since I want it to be a part of my server deployment.

Is there a way to install an ISAPI filter dll using a javascript or a vbscript?

If yes can someone give me pointers to the same?

Thanks,

Answer:

Ok... this is one of the frequently asked questions - how to programatically add and remove an ISAPI Filter.

If the question is in the context of an application setup/cleanup program, then my recommendation is to use WiX (Open Source project that simplifies creating MSI) to create the MSI installer package and simply leverage the built-in IIS Custom Action of WiX to add/remove ISAPI Filters. If you are not using MSI or WiX to deploying applications on Windows, you are likely re-writing your own version of a lot of existing, tested code.

Now, from the context of "how do I correctly add/remove an ISAPI Filter", I have written the following script tool in JScript and ADSI to illustrate how to add/remove an ISAPI Filter at the global or site level. It also enumerates details about configured ISAPI Filters (translated VBScript code from this blog entry into JScript) and works across remote servers. So... it's basically a one-stop tool for managing ISAPI Filters. :-)

Alrighty... I wrote this tool really quickly and haven't throughly tested it, but it should work well enough from IIS4 on up. Post comments if you find funny behavior so that future users know about it... and I can correct it.

Sample usage syntax:

  • FiltTool.js -server:AnotherServer
    enumerates all global filter status on machine named AnotherServer
  • FiltTool.js -site:W3SVC/1 -name:fpexedll.dll
    returns filter status for just fpexe.dll on website ID 1
  • FiltTool.js -site:W3SVC/1 -name:NewFilter -dll:%SYSTEMDRIVE%\Inetpub\scripts\Filter.dll -action:add
    adds %SYSTEMDRIVE%\Inetpub\scripts\Filter.dll as a filter with a name of NewFilter on website ID 1
  • FiltTool.js -name:NewFilter2 -dll:%SYSTEMDRIVE%\Inetpub\scripts\Filter.dll -index:2 -action:addadds %SYSTEMDRIVE%\Inetpub\scripts\Filter.dll as a global filter at index position 2 (i.e. third filter in FilterLoadOrder if there were at least two filters) with a name of NewFilter
  • FiltTool.js -site:W3SVC -name:OldFilter -action:remove -server:MyServer
    remove global filter named OldFilter on machine named MyServer

Now, some of the key implementation details I want to mention...

  • The FilterLoadOrder property determines whether IIS loads the filter. Thus, even if a filter node exists, if it's name is not in FilterLoadOrder, IIS will not attempt to load it.

    This is why Query enumerates the FilterLoadOrder property and NOT simply the global/site /Filters metabase node - I want to show realistic status of all Filter DLLs that IIS attempts to load.

    This is also why I modify the FilterLoadOrder property AFTER I add a Filter but BEFORE I remove a Filter. I need to make sure the filter node is A-OK before changing FilterLoadOrder to have IIS load it (in the site filter case), and I need to prevent IIS from accidentally using it when I am removing the filter.

  • Notice that the only relevant properties to manipulate for ISAPI Filter management are:

    1. FilterLoadOrder property of the appropriate global/site /Filters node
    2. A named node of type IIsFilter under the appropriate /Filters node
    3. FilterPath property of the named IIsFilter node
    4. IIS6 Only - FilterEnableCache property of the named IIsFilter node

In particular, please note that all other properties like FilterDescription, FilterState, Win32Error, etc are transient properties WRITTEN by IIS to convey runtime status. Thus, it is not useful for users to set those properties because IIS will overwrite them at runtime anyways.

  • Prior to IIS6, it was ok to have an identifier in the FilterLoadOrder property but no filter node with that identifier. On IIS6, it halts startup for security reasons.

Enjoy.

//David

 //
// Add/Remove/Enumerate global/site ISAPI Filters
//
// Origin : https://blogs.msdn.com/David.Wang
// Version: March 2 2006
//
String.prototype.EqualsIgnoreCase = EqualsIgnoreCase;

var ERROR_SUCCESS = 0;
var ERROR_INVALID_PARAMETER = 87;
var FILTER_OP_ADD = 1;
var FILTER_OP_REMOVE = 2;
var FILTER_OP_QUERY = 3;
var FILTER_ORDER_LAST = -1;

var CRLF = "\r\n";
var strHelp;
strHelp = "" +
    "Add/Remove/Enumerate global/site ISAPI Filters" + CRLF +
    CRLF +
    WScript.ScriptName + " [[-Parameter:Value]...]" + CRLF +
    CRLF +
    "Where:" + CRLF +
    "    Parameter  Value" + CRLF +
    "    ---------  -------------------------------------------" + CRLF +
    "    Name       Name of Filter to manipulate" + CRLF +
    "    DLL        Full DLL Pathname of Filter" + CRLF +
    "    Action     Add, Remove, or Query (Default is Query)" + CRLF +
    "    Server     Server to target (Default is localhost)" + CRLF +
    "    Site       Filter metapath - W3SVC for global," + CRLF +
    "               W3SVC/# for Site ID # (Default is W3SVC)" + CRLF +
    "    Index      [Add] Index to add filter (Default is LAST)" + CRLF +
    "";


var nReturnValue;
var nCommand;
var strServer;
var strMDPath;
var nLocation;
var strFilterName;
var strFilterPath;

nCommand = FILTER_OP_QUERY;
strServer = "localhost";
strMDPath = "W3SVC"
nLocation = FILTER_ORDER_LAST;

if ( ParseCommandline() &&
     ValidateOptions() )
{
    if ( nCommand == FILTER_OP_ADD )
    {
        nReturnValue = AddFilter( TrimSlashes( strMDPath ),
                                  nLocation,
                                  strFilterName,
                                  strFilterPath );
    }
    else if ( nCommand == FILTER_OP_REMOVE )
    {
        nReturnValue = RemoveFilter( TrimSlashes( strMDPath ),
                                     strFilterName );
    }
    else if ( nCommand == FILTER_OP_QUERY )
    {
        nReturnValue = QueryFilters( TrimSlashes( strMDPath ),
                                     strFilterName );
    }
    else
    {
        LogEcho( strHelp );
        nReturnValue = ERROR_INVALID_PARAMETER;
    }
}
else
{
    LogEcho( strHelp );
    nReturnValue = ERROR_INVALID_PARAMETER;
}

WScript.Quit( nReturnValue );

function ParseCommandline()
{
    var re = new RegExp( "-([^:]+):(.+)" );
    var arr;

    for ( var i = 0; i < WScript.Arguments.length; i++ )
    {
        arr = re.exec( WScript.Arguments( i ) );
        if ( arr == null )
        {
            LogEcho( "Invalid parameter " + WScript.Arguments( i ) )
            return false;
        }
        else
        {
            switch ( arr[ 1 ].toUpperCase() )
            {
                case "NAME":
                    strFilterName = arr[ 2 ];
                    break;
                case "DLL":
                    strFilterPath = arr[ 2 ];
                    break;
                case "ACTION":
                    nCommand =
                        arr[ 2 ].EqualsIgnoreCase( "ADD" ) ?
                            FILTER_OP_ADD :
                        arr[ 2 ].EqualsIgnoreCase( "REMOVE" ) ?
                            FILTER_OP_REMOVE :
                        arr[ 2 ].EqualsIgnoreCase( "QUERY" ) ?
                            FILTER_OP_QUERY :
                        nCommand;
                    break;
                case "SERVER":
                    strServer = arr[ 2 ];
                    break;
                case "SITE":
                    strMDPath = arr[ 2 ];
                    break;
                case "INDEX":
                    nLocation = parseInt( arr[ 2 ] );
                    break;
                default:
                    LogEcho( "Unknown parameter " + arr[ 1 ] );
                    return false;
            }
        }
    }

    return true;
}

function ValidateOptions()
{
    if ( nCommand == FILTER_OP_ADD &&
         ( strFilterName == null ||
           strFilterPath == null ) )
    {
        LogEcho( "Adding Filter requires the -Name and -DLL parameters." );
        return false;
    }

    if ( nCommand == FILTER_OP_REMOVE &&
         strFilterName == null )
    {
        LogEcho( "Removing Filter requires the -Name parameter" );
        return false;
    }

    return true;
}

function AddFilter(
    strMDPath,
    nLocation,
    strFilterName,
    strFilterPath
)
{
    var objFilters;
    var objFilter;
    var strFilterLoadOrder
    var strDelimiter;
    var i;
    var nCursor;
    var nOffset;

    try
    {
        objFilters = GetObject( "IIS://" + strServer + "/" +
                                strMDPath + "/Filters" );
    }
    catch ( e )
    {
        //
        // Failed to retrieve the Filters node for some reason.
        // Try to create it.
        //
        try
        {
            objFilters = GetObject( "IIS://" + strServer + "/" +
                                    strMDPath ).Create( "IIsFilters",
                                                        "Filters" );
            objFilters.SetInfo();
        }
        catch ( e2 )
        {
            //
            // Failed to retrieve and create the Filters node.
            // Bail.
            //
            LogEcho( FormatErrorString( e ) + CRLF +
                     "Failed to add Filter " + strFilterName +
                     " because Filters node cannot be created." );
            return e.number;
        }
    }

    try
    {
        //
        // Create the actual Filters node
        // Configure the FilterPath to point to DLL pathname
        //
        objFilter = objFilters.Create( "IIsFilter", strFilterName );
        objFilter.FilterPath = strFilterPath;
        //
        // On IIS6, if FilterEnableCache is not explicitly set to true,
        // Kernel Response Caching will be disabled for any request
        // that this filter runs on
        //
        //objFilter.FilterEnableCache = true;
        objFilter.SetInfo();
    }
    catch ( e )
    {
        if ( e.number == -2147024713 )
        {
            LogEcho( "Filter " + strFilterName + " already exists." );
        }
        else
        {
            LogEcho( FormatErrorString( e ) );
        }

        return e.number;
    }

    try
    {
        //
        // Update FilterLoadOrder so that IIS knows to load the filter
        //
        strFilterLoadOrder = objFilters.FilterLoadOrder;

        //
        // The Algorithm:
        // - Tokenize FilterLoadOrder on "," and first token index is 0.
        // - If token index matches desired index, insert filter at that
        // index.
        // - If desired index is -1 or desired index > token index, then
        // insert filter at the end.
        //
        strDelimiter = ( strFilterLoadOrder == "" ) ? "" : ",";
        i = 0;
        nCursor = 0;
        nOffset = 0;

        while ( nCursor != -1 )
        {
            if ( i == nLocation )
            {
                objFilters.FilterLoadOrder =
                    strFilterLoadOrder.substring( 0, nOffset ) +
                    strFilterName + strDelimiter +
                    strFilterLoadOrder.substr( nOffset );
                break;
            }

            nCursor = strFilterLoadOrder.substr( nOffset ).indexOf( "," );
            nOffset += nCursor + 1;
            i++;
        }

        if ( nLocation == FILTER_ORDER_LAST ||
             nCursor == -1 && i <= nLocation )
        {
            objFilters.FilterLoadOrder =
                strFilterLoadOrder + strDelimiter +
                strFilterName;
        }

        objFilters.SetInfo();
        LogEcho( "Added Filter " + strFilterName + CRLF +
                 strMDPath + "/Filters/FilterLoadOrder = " +
                 objFilters.FilterLoadOrder );
        return ERROR_SUCCESS;
    }
    catch ( e )
    {
        LogEcho( FormatErrorString( e ) );
        return e.number;
    }
}

function RemoveFilter(
    strMDPath,
    strFilterName
)
{
    var objFilters;
    var strFilterLoadOrder;
    var strNewFilterLoadOrder;

    try
    {
        objFilters = GetObject( "IIS://" + strServer + "/" +
                                strMDPath + "/Filters" );
    }
    catch ( e )
    {
        //
        // Failed to retrieve the Filters node for some reason.
        // Cannot delete filters
        //
        LogEcho( FormatErrorString( e ) + CRLF +
                 "Failed to remove Filter " + strFilterName +
                 " because Filters node cannot be read." );
        return e.number;
    }

    try
    {
        strFilterLoadOrder = objFilters.FilterLoadOrder;
        strNewFilterLoadOrder = "";

        nOffset = 0;
        nCursor = strFilterLoadOrder.substr( nOffset ).indexOf( "," );
        while ( nCursor != -1 )
        {
            if ( !strFilterName.EqualsIgnoreCase(
                 strFilterLoadOrder.substr( nOffset, nCursor ) ) )
            {
                strNewFilterLoadOrder +=
                    strFilterLoadOrder.substr( nOffset, nCursor + 1 );
            }

            nOffset += nCursor + 1;
            nCursor = strFilterLoadOrder.substr( nOffset ).indexOf( "," );
        }

        if ( !strFilterName.EqualsIgnoreCase(
             strFilterLoadOrder.substr( nOffset ) ) )
        {
            strNewFilterLoadOrder += strFilterLoadOrder.substr( nOffset );
        }

        objFilters.FilterLoadOrder = TrimCommas( strNewFilterLoadOrder );
        objFilters.SetInfo();
    }
    catch ( e )
    {
        LogEcho( FormatErrorString( e ) );
        return e.number;
    }

    try
    {
        objFilters.Delete( "IIsFilter", strFilterName );
        LogEcho( "Removed Filter " + strFilterName + CRLF +
                 strMDPath + "/Filters/FilterLoadOrder = " +
                 objFilters.FilterLoadOrder );
        return ERROR_SUCCESS;
    }
    catch ( e )
    {
        if ( e.number == -2147024893 )
        {
            LogEcho( "Filter " + strFilterName + " does not exist." );
        }
        else
        {
            LogEcho( FormatErrorString( e ) );
        }

        return e.number;
    }
}

function QueryFilters(
    strMDPath,
    strFilterName
)
{
    //
    // JScript version of the original script from
    // https://blogs.msdn.com/david.wang/archive/2006/02/11/HOWTO_Retrieve_and_Interpret_ISAPI_Filter_Status.aspx
    //
    // Adapted to query filter status
    //
    // Format of ISAPI Filters stored in IIS metabase
    //
    // IIS://localhost/W3SVC/Filters = Global Filters
    // IIS://localhost/W3SVC/#/Filters = Site Filters
    //
    // .../Filters/FilterLoadOrder = Order of Filters to load
    // .../Filters/Filter1
    //    .FilterPath = DLL location of Filter1
    //    .FilterState = Status of last filter load
    //    .FilterDescription = description set by filter
    //    .FilterFlags = events registered by filter
    //    .Win32Error = (IIS6 Only) Win32 Errorcode of last load
    //    .FilterEnableCache = (IIS6 Only) Hint that filter is cache friendly
    //
    // TODO:
    // Warn about Filters not in FilterLoadOrder
    //
    //

    var objFilters;
    var arr, i;

    if ( strFilterName != null )
    {
        return QueryFilter( strFilterName );
    }

    //
    // Check if there is a Filters node
    //
    try
    {
        objFilters = GetObject( "IIS://" + strServer + "/" +
                                strMDPath + "/Filters" );
    }
    catch ( e )
    {
        LogEcho( Site2String( strMDPath ) + " on server " +
                 strServer + " were not found." );
        return e.number;
    }

    arr = objFilters.FilterLoadOrder.split( "," );
    LogEcho( Site2String( strMDPath ) + " on server " + strServer );

    for ( i = 0; i < arr.length; i++ )
    {
        QueryFilter( arr[ i ] )
    }

    function QueryFilter(
        strFilterName
    )
    {
        var objFilter;

        try
        {
            objFilter = GetObject( "IIS://" + strServer + "/" +
                                   strMDPath + "/Filters/" + strFilterName );
            LogEcho(
                "+ Filter " + strFilterName + " - " +
                "\"" + objFilter.FilterDescription + "\"" + CRLF +
                "  Filter File = " + objFilter.FilterPath + CRLF +
                "  " + FilterState2String( objFilter.FilterState ) +
                CRLF +
                "  " + FilterFlags2String( objFilter.FilterFlags ) +
                "" );
        }
        catch ( e )
        {
            LogEcho( "+ Filter " + strFilterName + " does not exist!" + CRLF +
                     "  " + FormatErrorString( e ) );
        }
    }

    function Site2String(
        Site
    )
    {
        if ( Site.EqualsIgnoreCase( "W3SVC" ) )
        {
            return "Global Filters";
        }
        else
        {
            return "Filters for website " + Site;
        }
    }

    function FilterState2String(
        FilterState
    )
    {
        var strRetVal = "FilterState = " & FilterState;

        switch ( FilterState )
        {
            case 1:
                strRetVal += " (Loaded OK)";
                break;
            default:
                strRetVal += " (**ERROR**)";
                break;
        }

        return strRetVal;
    }

    function FilterFlags2String(
        FilterFlags
    )
    {
        var strRetVal = "FilterFlags = " + FilterFlags + CRLF;
        strRetVal += "  FilterEvent =";

        if ( FilterFlags & 32768 )      strRetVal += " ReadRawData";
        if ( FilterFlags & 16384 )      strRetVal += " PreprocHeaders";
        if ( FilterFlags & 8192 )       strRetVal += " Authentication";
        if ( FilterFlags & 4096 )       strRetVal += " UrlMap";
        if ( FilterFlags & 2048 )       strRetVal += " AccessDenied";
        if ( FilterFlags & 64 )         strRetVal += " SendResponse";
        if ( FilterFlags & 1024 )       strRetVal += " SendRawData";
        if ( FilterFlags & 512 )        strRetVal += " Log";
        if ( FilterFlags & 128 )        strRetVal += " EndOfRequest";
        if ( FilterFlags & 256 )        strRetVal += " EndOfNetSession";
        if ( FilterFlags & 67108864 )   strRetVal += " AuthComplete";
        strRetVal += CRLF;
        if ( FilterFlags & 1 )
        {
            strRetVal += "    Listens on SecurePort" + CRLF;
        }
        if ( FilterFlags & 2 )
        {
            strRetVal += "    Listens on NonSecurePort" + CRLF;
        }
        if ( FilterFlags & 524288 )
        {
            strRetVal += "    Runs as High Priority" + CRLF;
        }
        if ( FilterFlags & 262144 )
        {
            strRetVal += "    Runs as Medium" + CRLF;
        }
        if ( FilterFlags & 131072 )
        {
            strRetVal += "    Runs as Low Priority" + CRLF;
        }

        return strRetVal;
    }

    return ERROR_SUCCESS;
}

function TrimSlashes(
    strInput
)
{
    return strInput.replace( new RegExp( "^/+|/+$", "g" ), "" );
}

function TrimCommas(
    strInput
)
{
    return strInput.replace( new RegExp( "^,+|,+$", "g" ), "" );
}

function LogEcho(
    strText
)
{
    WScript.Echo( strText );
}

function EqualsIgnoreCase(
    strOther
)
{
    return this.toString().toUpperCase() ==
           strOther.toString().toUpperCase();
}

function Int32ToHRESULT( num ) {
    if ( num < 0 )
    {
        return "0x" + new Number( 0x100000000 + num ).toString( 16 );
    }
    else
    {
        return "0x" + num.toString( 16 );
    }
}

function FormatErrorString(
    objError
)
{
    return "(" + Int32ToHRESULT( objError.number) + ")" + ": " +
           objError.description;
}