IIS 7 C++ Module API Sample: Changing Handlers

IIS 7 C++ Module API Sample: Changing Handlers

Introduction

Today's modules looks at how you can programmatically change handlers. RQ_EXECUTE_REQUEST_HANDLER is by far the most common notification used in the IIS 7 pipline. This makes sense because it is where the response is generated. We know how many different response generation techonologies there are out there. The RQ_EXECUTE_REQUEST_HANDLER notification is only delivered to the modules configured in system.webServer/handlers for the request. This is unlike all other request (RQ_*) notifications, where all modules in system.webServer/modules, that have registered for a notification receive it. The system.webServer/handlers configuration syntax is pretty flexible, but obviously cannot do everything for everyone. This post assumes you've already got the hello world module working, so it jumps straight into API discussion.

Pipeline

Here is the first portion of request pipeline. (from the top of httpserv.h) You can see there's an entire notification dedicated to handler selection. You can also see that by this point in the pipeline the request is already authenticated and authorized.
#define RQ_BEGIN_REQUEST               0x00000001
#define RQ_AUTHENTICATE_REQUEST        0x00000002
#define RQ_AUTHORIZE_REQUEST           0x00000004
#define RQ_RESOLVE_REQUEST_CACHE       0x00000008
#define RQ_MAP_REQUEST_HANDLER         0x00000010
#define RQ_ACQUIRE_REQUEST_STATE       0x00000020
#define RQ_PRE_EXECUTE_REQUEST_HANDLER 0x00000040 
#define RQ_EXECUTE_REQUEST_HANDLER     0x00000080
...

Deciding When to Change Handlers

The whole reason we're writing this module is because the built-in system.webServer/handlers configuration does not meet our needs. So we need our own logic. In this case we're going to target some special logic that Ruby on Rails seems to like: deliver that which can be delivered and render the rest unto Rails. First we check if we have a IScriptMapInfo already associated with the request. If we do it means that system.webServer/handlers could find a handler. If no handler could be found it means we should deliver it to Rails. If we can find a handler then we need some additional checks: first we use IHttpContext::GetFileInfo to check if this request maps to a file on the file system. Finally, some handlers do not need file system backing, so we compare IScriptMapInfo::GetResourceType to 3. (3 is from the system.webServer/handlers definition in %WinDir%\system32\inetsrv\config\schema\iis_schema.xml)
MODULE::OnMapRequestHandler(
    IHttpContext*           pContext,
    IMapHandlerProvider*    pProvider
)
{
    ...

    IScriptMapInfo* pScriptMap = pContext->GetScriptMap( );

    ...

    if( NULL != pScriptMap )
    {
        if(
            NULL != pContext->GetFileInfo( ) ||
            3 == pScriptMap->GetResourceType( )
        )
        {
            //
            // if we have a script map AND
            //      - a file info
            //      - or resourceType=Unspecified (3)
            // then no need to change handlers
            //
            goto Finished;
        }
    }

Choosing the New Handler

We now know that we want to deliver the request to Rails. One solution at this point would be to implement IScriptMapInfo and set the IScriptMapInfo::GetModules to be our Rails handler module. The problem with this solution is that there is more than one way to run Rails on IIS, so we don't know which module should handle Rails. So we need to expose some configuration to the web server administrator. At this point we could get into schema extensibility, but there's already a perfectly good configuration section that does almost exactly what we want: system.webServer/handlers! :-). So we tweak the URL to make it look like something system.webServer/handlers would understand (replace the stuff after the last slash with index.rb) and then we ask IIS to do the work for us:
    //
    // Get handler for pszScriptName/APPEND_PATH
    //
    hr = pContext->MapHandler(
        pSite->GetSiteId( ),
        pSite->GetSiteName( ),
        pszFullAppPath,
        pRequest->GetHttpMethod( ),
        &pScriptMap,
        FALSE
    );
    if( FAILED( hr ) )
    {
        goto Finished;
    }

    //
    // Now make that the handler for this request
    //
    pProvider->SetScriptMap( pScriptMap );

Deployment

Since this is a C++ module it needs to be in system.webServer/globalModules. Since this module uses RQ_* notifications is need to be in the application module list (system.webServer/modules). This later requirement means you now have a method for turning this custom handler selection on/off, cause system.webServer/modules is configurable at the application level. (e.g. only add the module in web.configs where you want Rails-style handler mapping). Finally, cause we're using the system.webServer/handlers configuration, we should add an entry for *.rb, mine looks like:
    <location path="" overrideMode="Allow">
        <system.webServer>
            <handlers accessPolicy="Read, Script">
                ...
                <add 
                    name="rails (currently broken)" 
                    path="*.rb" 
                    verb="GET,HEAD,POST" 
                    modules="FastCgiModule" 
                    scriptProcessor="c:\ruby\ruby.exe" 
                    resourceType="Unspecified" 
                />
                ...

Expected Behaviour

  • http://localhost/a.rb should get an HTTP 500 with detailed error of "<handler> scriptProcessor could not be found in <fastCGI> application configuration." (I still haven't got Rails working as a FastCGI on IIS 7, I'll blog about it when I do)
  • http://localhost/app1/recipe/new should get the same HTTP 500 if our module is running and a regular HTTP 404 if not
  • "Failed Request Tracing" log should have something like this:
    NOTIFY_MODULE_START 
        ModuleName="RubynatorModule"
        Notification="MAP_REQUEST_HANDLER"
    HANDLER_CHANGED 
        OldHandlerName="StaticFile"
        NewHandlerName="rails (currently broken)"
        NewHandlerModules="FastCgiModule"
        NewHandlerScriptProcessor="c:\ruby\ruby.exe"
        NewHandlerType=""
    NOTIFY_MODULE_END 
        ModuleName="RubynatorModule"
        Notification="MAP_REQUEST_HANDLER"
    

Getting Ruby on Rails Working

This is beyond the scope of this article, checkout this excellent wiki entry.

No Comments