Channels ▼
RSS

Web Development

WebDAV, IIS, & ISAPI Filters

Source Code Accompanies This Article. Download It Now.


Nov00: WebDAV, IIS, & ISAPI Filters

Martin is a consultant for the Scandinavian Internet Technology. He can be contacted at marty@site.se.


Sharing information is one of the major features of the InteFrnet and the World Wide Web. But when it comes to ways of dealing with the flow of information in a structured and safe manner, there's room for improvement. While HTTP has been great for retrieving data, it lacks support for file locking, custom file/directory properties, directory creation, and file/directory copying and deletion -- all of the things you can do on your computer. In this article, I will introduce you to Web-based Distributed Authoring and Versioning (WebDAV) and the ISAPI filter architecture by implementing a filter with some rough WebDAV capabilities.

The WebDAV specification defines a set of extensions to the HTTP protocol that lets you collaboratively edit and manage files on remote web servers. One way to look at it is that WebDAV makes a web site look like a local drive on your computer. In other words, WebDAV is essentially the HTTP protocol, with some additional methods and headers, which makes it possible to access a web server in a way similar to that in which you use directories and files on your PC. And with its support for locking and versioning of files, WebDAV also becomes a tool for collaboration. Table 1 lists these WebDAV extensions to HTTP. I use the term "WebDAV method" to refer to an extension that is available with WebDAV, but not HTTP.

Tools such as FrontPage and NaviPress also extend the HTTP protocol to achieve some of these functions. However, they've done so in a proprietary way that makes communication between products from different vendors impossible. WebDAV, on the other hand, is managed by an IETF working group (http://www.webdav.org/) and is supported by Microsoft, Netscape, and many other companies. In fact, Microsoft has implemented WebDAV in Internet Explorer 5, Web Folders, and Internet Information Server 5.

The ISAPI Filter Architecture

A common web-server feature is the ability to let you add (or modify) functionality using plug-ins. Microsoft's Internet Information Server (IIS) 4 is no exception. For this purpose, Microsoft developed the Internet Server API (ISAPI), which lets you extend any ISAPI-compliant web server, including Microsoft's own Internet Information Server.

There are two different types of ISAPI components -- extensions and filters. Extensions are like CGI scripts that are called when the client has completed its request and are referenced with a normal URL (for example, http://myserver.se/myextension .dll). Filters, on the other hand, are more interesting. Unlike extensions, you cannot call a filter directly. Instead, it receives notifications from the web server when certain events occur (see Table 2) in the lifetime of a client request. When the IIS service is started, the filter is loaded into memory and informs the web server about the type of events it would like to get notification for, the priority the server should give that filter, and whether the filter should be notified under a secure connection. (I use the term "interested filter" to refer to filters that have registered for notifications for one or more given events.) Both extensions and filters are compiled into DLLs and installed using Internet Service Manager.

To simplify ISAPI development, you can use MFC, which has a number of support and wrapper classes. To build an ISAPI filter, you would extend the class CHttpFilter, then override the notification functions that you have registered interest in. I use MFC in the filter I build in this article. The complete source code for this filter is available electronically; see "Resource Center," page 5.

ISAPI components run in the same address space as the HTTP server so there is usually little overhead in calling them. This also means that they inherit many attributes from their parent process -- the HTTP server process -- such as its security context and the fact that they run in a service context. Additionally, this also means that all code must be thread safe because IIS can serve multiple concurrent connections to a resource. If you use MFC, you can use its synchronization classes to resolve that problem.

Here's how a typical request might manifest itself with different notifications to the interested filters: First, IIS reads a block of data and notifies interested filters with an OnReadRawData notification. This block will contain the request line and some headers. In most cases, the block size will be sufficiently large to contain all the headers. In some cases, it also contains some request body data. If all headers weren't included in the block, IIS reads as many blocks as it takes to include all the headers. With every block it reads, it also notifies interested filters with OnReadRawData. When all headers have been received, the server parses the headers and notifies interested filters with OnPreProcHeaders. This allows a filter to inspect, change, and add headers. Then IIS notifies interested filters with OnUrlMap, which indicates that IIS has mapped the resource in the request line to a physical path on the server. The filter can change this physical path if it so desires. If the client has supplied a request body, it is read. This results in one or more notifications of type OnReadRawData to interested filters. Additionally, there are a few more notification types that involve events such as authentication and logging. If you are interested in them, see the ISAPI filter documentation (http:// msdn.microsoft.com/ library/psdk/iisref/isgu3vn7.htm).

Two arguments are given to every notification function. The first is always an instance of the CHttpFilterContext class, which is basically a wrapper around the HTTP_FILTER_CONTEXT structure. This object provides services that let the filter communicate with its context, such as getting a particular server variable or writing raw data to the client. The second argument is a pointer to a structure, which is specific for every type of notification. In OnReadRawData, for example, you receive a pointer to an HTTP_FILTER_RAW_ DATA structure that contains the data that has been read from the client.

The return code of every notification function tells IIS how to proceed with the notification chain. For example, the filter could instruct the server to close the connection with the client, it could ask the server to read another block of data from the client, or just tell the server to notify the next filter.

The Filter Implementation

WebDAV consists not only of the methods in Table 1, but also of the entire HTTP 1.1 protocol. You could say that WebDAV extends HTTP in the same way that a Java class can extend another class. WebDAV contains all the HTTP functionality, and many of the new functions share traits with the old functions. For example, if a client requests an HTTP property (such as an ETag or Content Type) for a resource in a PROPFIND request, they must be identical to the ones returned by a GET or HEAD. In this case, these latter methods will be implemented by IIS and the new methods such as PROPFIND are implemented by our filter, so you need a tight integration with IIS to find out what Content Type or ETag a certain resource has. It turns out that this information can sometimes be impossible to retrieve in an efficient manner.

To serve a WebDAV request, you need to have access to the complete request, which consists of two things: the request headers and the request body. Unfortunately, the ISAPI filter architecture does not have an event type that informs you when the HTTP server has read all the headers and the entire body and gives you access to this data. Instead you have to rely primarily on OnReadRawData, OnPrecProcHeaders, and OnURLMap for this purpose.

But recall that data from the request can be scattered across many calls to OnReadRawData. How do you patch all of these blocks together? The answer can be found in one of the members of CHttpFilterContext. In contrast to CHttpFilter (which is only instantiated once), a new CHttpFilterContext object is created for every TCP session with a client. The structure that CHttpFilterContext wraps, HTTP_ FILTER_CONTEXT, is exposed as a public member variable in CHttpFilterContext and contains a void pointer. This pointer can be used for any context information that the filter wants to associate with a particular request. This lets you keep a context object for every request, which can contain all the data you have accumulated for that particular request, as well as other information for that request.

However, OnReadRawData is called for every type of request. The filter should only handle WebDAV requests. As much as you might respect the original HTTP methods, you don't want anything to do with them for performance reasons. These methods are implemented by IIS and any interference by you will slow down the process. So what you need is a way to identify a WebDAV request as early in the request process as possible, and step out of the way if it isn't. This can be done in OnPreProcHeaders, which lets you inspect the headers of the request including the request method. If the method is a WebDAV method, you can set a flag in the context object indicating to OnReadRawData that this is a WebDAV method and that it should copy the data it receives from now on.

However, there is still a problem. The block of data contained in the OnReadRawData event just before OnPreProcHeaders can also contain request body data in addition to header data. So if you decide that you're interested in the request in OnPreProcHeaders, you might have missed some data.

A solution is to have OnReadRawData collect data by default. And if processing in OnPreProcHeaders reveals that the method is a standard HTTP method, then we set a flag telling OnReadRawData to ignore the rest of the data for this request. When the request has finished, you reset the flag. How do you know when a request has ended? You don't, because IIS gives filters notification when the TCP connection has been closed -- not when a request has ended. And multiple requests can be sent over one connection through the use of HTTP Keep-Alives. A possible resolution would be to disable Keep-Alives on the server, though this is not an acceptable solution.

It seems then that the only way out is to copy every incoming block of data in OnReadRawData. On the bright side, if you don't know for certain that the current method is WebDAV, you don't accumulate the data. Instead, you overwrite the old block. This approach assumes that all the header data will be included in the first block of data. The risk involved in this assumption will probably fade a bit when you consider that the amount of data that IIS attempts to read every time is 48 KB, which should be enough for any requests header data.

But there are still more problems. The HTTP method OPTIONS is used by the client to retrieve information concerning the communication abilities for a particular resource or for the server as a whole. As a response, the server can (and IIS does) return a Public header, which lists all the methods supported by the server, and an Allow header, which lists all the methods supported for a particular resource. Since the server now also handles WebDAV methods, you should include the names of these methods in the appropriate headers in an OPTIONS response. You must also include an additional DAV header telling the client that this is a WebDAV-compliant server. Although CHttpFilterContext provides a member function for adding headers to a response, CHttpFilterContext::AddResponseHeaders, it does not let you change an existing header.

One way of doing all of this is to handle the OPTIONS method yourself, just as you have done with the WebDAV methods. This seems excessive though, since all you really need to do is modify a couple of headers. An alternative is to flag for an OPTIONS request in OnPreProcHeaders. When IIS sends its response, you intercept it in OnSendRawData. Unfortunately, you cannot change the data that will be sent. But what you can do is copy the data to a temporary buffer, add the data you want, send the temporary buffer to the client using CHttpFilterContext::WriteClient, and return a value telling IIS that you have handled the notification and to refrain itself from sending the unmodified OPTIONS response to the client.

The MetaBase and Active Directory

ISAPI provides moderate integration with the web server. Using the CHttpFilterContext object provided with each notification function, you can retrieve the server variables defined by the CGI specification as well as some Microsoft-specific ones. You can also set the next read size, and add headers that will be included in a 401 Access Denied response. But that's basically it. For most filters, this will be more than enough. But for a filter like this, which operates extremely close with, and needs access to, many internals of the web server, you need more.

For example, consider the PROPFIND method. If a client requests the MIME type for a particular resource in a PROPFIND request, the filter would have to know how to map a file extension to a MIME type. Sure, the filter could have its own MIME map with the most common types. But this would not only be poor design, it would also be error prone. If users add another MIME type or change an existing MIME type in the IIS administration program, your filter would never know about it and return an invalid MIME type.

The solution is in the MetaBase, the database where IIS stores most of its configuration information. The MetaBase is organized in a hierarchical structure, much like the Windows Registry, with keys containing subkeys and properties. For that matter, much of what you can find in the Internet Service Manager is stored in the MetaBase. (For MetaBase details, see http:// msdn.microsoft.com/library/psdk/iisref/ aint94dh.htm.)

The MetaBase is accessed through Active Directory Service Interfaces (ADSI), a set of COM interfaces generalizing the capabilities that are offered by many different directory services such as LDAP and NDS from Novell. If you are familiar with Java Naming and Directory Interface (JNDI), ADSI basically provides the same functionality.

Both ADSI and JNDI are based upon the notion of providers that conform to a set of specifications and provide access to different directory services. Access to a directory object is given through the use of a URL, which also tells the Active Directory engine what provider is to be used. Every directory object also has an associated class, or schema class, which is to a directory object what the Java class Class is to a Java object. Through the schema class you can discover at run time what properties a directory object must contain, what properties a directory object may contain, if the directory object is a container (if it can contain other directory objects), and more. It is also possible for a schema class to be derived from other schema classes.

The IIS documentation reveals that the name of the schema class for the root object in the MetaBase is IIsComputer. Examining the documentation for that schema, you learn that directory objects of that type can contain an object of type IIsMimeMap, which always contains a property by the name of MimeMap -- a list of the MimeTypes known to the system. But this property is inheritable, which means that web servers, virtual directories, or even files can override this property in the MetaBase. This means that to perform a correct MIME type mapping, you must know which resource was requested and then traverse up the MetaBase tree looking for a match in every branch. Considering the effort required to implement this compared to the advantages gained, I have decided not to do this in this version. Listing One presents the complete MIME mapping function.

Conclusion

There certainly remains a lot of work on the filter I present here to consider it a commercial alternative. The PROPPUT command is not implemented yet, memory pools need to be set up for efficiency purposes, certain parts of the code need to be made thread safe, and so on.

DDJ

Listing One

/* Returns a mime type for a file NOTE: Not threadsafe */
BOOL GetMimeType(char* szFile, char* szBuff) {
//Find extension
    char* szExt = szFile;
    while(*szExt != '\0' && *szExt != '.') {
        szExt++;
    }
    //If we found it, search the MetaBase for it
    if(*szExt != '\0') {
        _bstr_t bstrExt(szExt);
        static CComPtr<IADs> adMimeMap;
        static bool bDidInit = false;
        static bool bGoodObject = false;
        static VARIANT vMimeMap;
        HRESULT hr;
        //Look up the mimemap object that IIS has
        if(!bDidInit) {
            bDidInit = true;
            //Reference MimeMap object through a URL with "IIS" as provider
            hr = ::ADsGetObject(L"IIS://LocalHost/MimeMap", 
                                             IID_IADs, (void**)&adMimeMap);
            if(SUCCEEDED(hr)) {
                //Get the "MimeMap" property
                ::VariantInit(&vMimeMap);
                hr = adMimeMap->GetEx(L"MimeMap", &vMimeMap);
                if(SUCCEEDED(hr)) {
                    //It should be an array
                    if(vMimeMap.vt & VT_ARRAY) {
                        bGoodObject = true;
                    }
                }
            }
        }
            //Only proceed if we have a valid reference
        if(bGoodObject) {
            //Declare some nifty variables
            long lIdx;
            long lHigh;
           _bstr_t bstrBuff;
            BSTR bstrRaw;
            CComQIPtr<IISMimeType> MimeType;                    
            VARIANT vElem;
            ::VariantInit(&vElem);
            SAFEARRAY* pArrMimeMap = vMimeMap.parray;
            //Get the upper and lower bounds of the array
            ::SafeArrayGetLBound(pArrMimeMap, 1, &lIdx);
            ::SafeArrayGetUBound(pArrMimeMap, 1, &lHigh);
            //Loop through the array
            while(lIdx <= lHigh) {
                hr = ::SafeArrayGetElement(pArrMimeMap, &lIdx , &vElem);
                if(SUCCEEDED(hr)) {
                    if(vElem.vt & VT_DISPATCH) {
                        //Each element in array should be of type IISMimeType
                        MimeType = vElem.pdispVal;
                        if(MimeType != NULL) {
                            MimeType->get_Extension(&bstrRaw);
                            bstrBuff = bstrRaw;
                            //Check if the extension matches
                            if(bstrExt == bstrBuff) {
                                MimeType->get_MimeType(&bstrRaw);
                                bstrBuff = bstrRaw;
                                strcpy(szBuff, (char*)bstrBuff);
                                MimeType.Release();
                             return TRUE;
                            }
                            
                        }
                        MimeType.Release();
                    }
                }
                lIdx++;
}   // Loop through elements
        } //bGoodObject
    }   //szExt != \0
    *szBuff = '\0';
    return FALSE;
}





Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV