|  | Click Here!

Search the site:
EXPERT SEARCH

| Programming Languages
| Databases
| Security
| Web Services
| Network Services
| Middleware
| Components
| Operating Systems
| User Interfaces
| Groupware & Collaboration
| Content Management
| Productivity Applications
| Hardware
| Fun & Games

EarthWeb sites
Crossnodes
Datamation
Developer.com
DICE
EarthWeb.com
EarthWeb Direct
ERP Hub
Gamelan
GoCertify.com
HTMLGoodies
Intranet Journal
IT Knowledge
IT Library
JavaGoodies
JARS
JavaScripts.com
open source IT
RoadCoders
Y2K Info

- 13 — Developing Web Applications Using ISAPI Extensions

- The Nature of Web Servers and CGI
- How CGI Scripts Work
- How Extensions Are Microsoft's Answer to CGI

- Extension Architecture

- The Extension API Set and Associated Data Structures
- REGBROWSE: A Server Directory Browser

- Getting the Extension Running

- Debugging ISA Extensions
- Moving On

#
13 — Developing Web Applications Using ISAPI Extensions

by Kevin Walsh

My wife recently made an avocado salad, which left her with two avocado seeds, light tan and slightly larger than a robin's egg. She was reluctant to throw the seeds away and asked me if I knew how to grow an avocado tree from a seed. As it became clear to me that she was serious, my thoughts were ones of sadness as I pictured two forlorn seeds sitting alone, unloved and sterile in a pot, as unidentifiable organisms made themselves at home around them because she had no clue about how to make them grow. The next day, as a lark, I decided to conduct an Internet search on the topic.

As the well-connected user you no doubt are, you're probably familiar with the ever-expanding selection of Web search engines on the Internet. Just fire up your favorite Web browser, click the Search button, and you'll see at least five search tools to choose from, each with a slightly different approach to sifting through the vast amount of data on the Internet. These search engines are surprisingly fast, and my search on "avocado" resulted in hundreds of hits. To my naive astonishment, an awful lot of people care deeply enough about avocados to dedicate the time and effort to create some really quite attractive Web pages about the sublime pleasures and mercantile advantages of the strange green fruit. My wife got her instructions, and I became intrigued with these applications that somehow buried themselves in a Web page.

###
The Nature of Web Servers and CGI

Most people readily grasp the notion of HTML tags in a document and how they control the display of text. The language is simple and powerful, and after an a few hours of applied study, even the most non-technical among us can construct nice-looking documents, complete with hyperlinks to other sections of the page and even to other pages. Technophiles, after playing with a browser for just a few minutes, intuitively understand that a Web browser is really an HTML interpreter and display engine, with a specific network protocol designed to transfer data contained in documents stored on a server—a souped-up FTP, if you will. Maybe it was its deceptive simplicity that caused many of us to dismiss the Web as a serious applications deployment environment; even Bill Gates admits that the explosive growth of the Web took Microsoft by surprise.

In retrospect, the clues were there almost from the beginning. As more and more server-based bits of magic like those search engines came online, I began to realize that suddenly, everything I knew was wrong. I had to find out what made that magic work, or get left behind. In a Web server, that magic is known as the Common Gateway Interface—CGI—and it's really not magic at all. Like browsers and HTML, it's surprisingly simple. Web server–based applications and CGI challenge developers to rethink how they implement and deploy client/server applications. This technology can save a great deal of the down-and-dirty grunt work now taken for granted as the price getting two machines to talk to each other. It allows developers to forget about the nuts and bolts of network programming and concentrate on the job at hand.

As an example, I recently completed a job for a security firm that had installed employee ID card scanners to control access to a steel plant and needed to gather information from the card readers to select employees for random drug testing. The question was, how would they get the computer at the medical clinic to connect to the server that runs the security system and get that computer to ask for a list of employees in the plant? I chose to implement a client/server application using the MFC CSocket classes. The client application sent a series of messages to the server, which in turn connected to the security controller. The controller shipped a list of people in the plant at that time to the server, which picked some people at random and passed the lucky winners back to the client. None of this was rocket science, but I did have to invent a small messaging protocol based on command/response packets, along with a simple state machine implementation at both ends to respond appropriately to incoming packets. After a lot of tweaking to ensure synchronization between the front and back ends, I finally got the thing installed and running, so I suppose the steel company now has a steady stream of onsite workers filling their quota. Although I was pretty pleased with the implementation, I wondered if perhaps there wasn't a better way. Well, there is a better way, and it's the Web.

###
How CGI Scripts Work

CGI is a relatively simple standard that defines how an HTTP server process can launch an application and pass data from the client's current HTML document to it by using environment variables and the standard input (stdin in Unix speak.) The application usually has the ability to get at resources the client either can't read or can't access because of security restrictions. The results of the application's work are passed back to the client by using the standard output (stdout) in the form of HTML tags generated on-the-fly to allow the users' browser to present the data. The application might be a standard executable, a batch file, or a Perl script; anything that can parse a command line or get environment variables can be a CGI extension. A typical CGI operation, such as a query command within an HTML document, might look something like the following:

    Click <A HREF="http://BigServer/cgi-bin/avosrch.exe?query=AllAvocados">here </A> [ic:ccc]

    to find out just how many people care about avocados.

Embedded in the plain text is a URL that identifies the server BigServer. The string cgi-bin tells the server where to look for the script that can handle the request, and avosrch.exe is the script itself, in this case an executable file. The server uses the question mark after the script name to figure out where the filename ends and the data to be passed to the script begins. It's up to the script to figure out what the data means and what to do with it. The server creates a separate process for the executable to run in passes query=AllAvocados to it in the QUERY_STRING environment variable. Listing 13.1 shows another common way to pass data to a CGI script using HTML forms. Forms such as these gather user input with dialog controls like text fields and buttons.

Listing 13.1. Invoking a CGI script with HTML forms

    <H1>Choose which types of avocados you are desperately interested in:</H1>

    <FORM ACTION="http://BigServer/cgi-bin/avosrch.exe" METHOD=POST>

    <INPUT TYPE="checkbox" name="c1" value="SmallAvocados"> Small Avocados

    <p>

    <INPUT TYPE="checkbox" name="c2" value="LargeAvocados"> LargeAvocados

    <p>

    <INPUT TYPE="checkbox" name="c3" value="ReallyBigAvocados"> Really Big Avocados <p>

    <INPUT TYPE="checkbox" name="c4" value="AllAvocados"> All Avocados <p>

    <INPUT TYPE="submit" value="Submit">

    <p>

    </FORM>

Listing 13.1 shows a bit of HTML code that defines a command button and set of checkboxes and associates values with them. The <ACTION> tag tells the server what script to execute. The <METHOD> tag indicates a POST operation, which tells the server that the data should be placed on the standard input in the form Name=Value, where "Name" is the name of the input field and "Value" is the data associated with it. If, for example, the first checkbox is selected, the value c1=SmallAvocados is sent to the script. If the <METHOD> tag had indicated a GET operation, the HTTP server would place the data in an environment variable called QUERY_STRING. Other commonly used variable names include REQUEST_METHOD, PATH_INFO, and PATH_TRANSLATED.

###
How Extensions Are Microsoft's Answer to CGI

Simple though it is, the development of CGI has resulted in an extraordinarily rich and diverse set of applications deployed on Web servers worldwide, from efficient Web page search engines to an online phonebook for everyone in the United States. (I must admit that it's a little spooky to see my name pop up on a server available to anyone in the world.) The explosion in server applications has been more than matched by an explosion of users all clamoring to use these applications, which in turn has forced server administrators to beef up the machines running these applications to handle the load.

Compared to CGI, Microsoft's IIS (Internet Information Server) is quite new, but there are some advantages to joining the party late. IIS does implement CGI, so CGI scripts and their associated HTML documents will work just fine. To address the drawbacks inherent in the CGI process-based model, Microsoft has produced the ISAPI Extension architecture, which maintains the spirit of CGI even as it provides a much-needed performance boost, especially for heavily accessed servers.

####
Extension Architecture

Unlike CGI, an ISAPI extension is a standard Windows Dynamic Link Library (DLL) that runs in the process space of the IIS server, which avoids the overhead of creating a separate process for every connected user. The IIS server can load extensions on startup and can unload a particular extension if it's had no activity after a given period of time. The same instance of an extension DLL is used for all client connections, so as client connections increase in number, an IIS server uses far fewer resources than a comparable Unix-based CGI server.

There are some drawbacks to running in the server's address space. Because only a single instance of an extension DLL is ever loaded and several clients may be exercising your extension at exactly the same moment, your DLL must be multithread safe. This means that access to static or global data in a DLL must be synchronized using such things as semaphores, critical sections, or mutexes. (Microsoft suggests that you keep your extension processing as short as possible anyway, so that the client isn't waiting around too long for results.) Finally, a "buggy" extension can crash the server. That shouldn't cause too much of a problem, since no one writes buggy code, right?

Living in the server's address space calls for some different ways to pass client data to the extension. Happily, CGI-based extensions, if written as a native executable, can be easily modified to act as an ISAPI extension, usually by simply changing it to a DLL and adding a few required exported functions. If you have CGI extensions in some other form, such as batch files or Perl scripts, the ISAPI SDK offers a set of sample code that shows how an ISAPI extension can provide a wrapper for them so that they can run unchanged. As for the source HTML document, everything works the same way. When IIS sees a URL with an .exe or .bat filename extension, it launches it as a CGI script. If it's a .dll, it tries to load it as an ISA extension, so of course you need to change your CGI script name in the URL to reflect that it's a DLL.

To illustrate, try constructing a simple ISAPI extension that browses directories on the HTTP server. When you're done, you will have accomplished the following:

- Constructed an HTML home page that clients can access to generate requests.
- Constructed an Internet Server Application (ISA) DLL.
- Provided implementations and exports for the GetExtensionVersion and HttpExtensionProc functions required by the Internet Information Server (IIS).
- Exercised the small set of ISAPI functions to read and send data to the requester.
- Shown how to build HTML on-the-fly to communicate the results of the ISA's work.

Most of these features are required from any ISA DLL, no matter what task you have in mind. However, before you start the actual code, you need to know the tools, just like any good carpenter. Take a few minutes to review the ISAPI functions and data structures before you skip to the code. Also, if you haven't already done so, now would be a good time to install the INetSDK on your development machine and make sure the IIS server software is available to test your extensions. The ideal configuration is a machine with Windows NT Advanced Server, the IIS server software, the INetSDK, and your compiler of choice installed. After all, disk space is cheap nowadays.

###
The Extension API Set and Associated Data Structures

All extension APIs and data structures are defined in httpext.h in the INetSDK\Include directory. When the ISA server loads an extension for the first time, it looks for an exported function called GetExtensionVersion, which all extensions must implement. This function indicates to the server which version of the ISA specification the extension was written for to provide backward compatibility in future ISA releases. Here is the function prototype:

    BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer);

The pVer argument is a pointer to a buffer containing a structure called HSE_VERSION_INFO. The actual definition of the structure, shown in Listing 13.2 is quite simple.

Listing 13.2. HSE_VERSION_INFO structure definition.

    typedef struct _HSE_VERSION_INFO

    {

       DWORD dwExtensionVersion;

       CHAR  lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN];

    }HSE_VERSION_INFO, *LPHSE_VERSION_INFO;

All implementations of this function should set the dwExtensionVersion member of the structure to a constant supplied in the INetSDK header file. The other member is a string for including some descriptive information about your extension. A sample suitable for cutting and pasting is shown in Listing 13.3.

Listing 13.3. Sample implementation of GetExtensionVersion.

    BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)

    {

       pVer->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR);

       lstrcpyn(pVer->lpszExtensionDesc, "Copyright 1996, MuchoAvocado Inc.",

    HSE_MAX_EXT_DLL_NAME_LEN);

       return TRUE;

    }

You've seen that CGI communicates with its extensions by loading up a known set of environment variables with data from the client. Since ISAPI extensions are DLLs that run in the address space of the HTTP server, there's no need for such a roundabout mechanism. Instead, ISA communicates with extension DLLs through a data structure called an Extension Control Block (ECB). The ECB is passed to extensions with the function HttpExtensionProc, which all extensions must export:

    DWORD HttpExtensionProc( LPEXTENSION_CONTROL_BLOCK *lpEcb);

This function is called when the server determines that a client has a request the extension should handle.

The only argument is a pointer to a server-provided buffer containing the Extension Control Block. For most applications, the ECB will contain all the information needed to carry out the client's request. The structure in Listing 13.4 will look pretty familiar to CGI developers. The members are described in Table 13.1.

Listing 13.4. Extension Control Block structure definition.

    typedef struct _EXTENSION_CONTROL_BLOCK

    {

       DWORD    cbSize;

       DWORD    dwVersion;

       DWORD    connID;

       DWORD    dwHttpStatusCode;

       LPSTR    lpszLogData;

       LPSTR    lpszMethod;

       LPSTR    lpszQueryString;

       LPSTR    lpszPathInfo;

       LPSTR    lpszPathTranslated;

       DWORD    cbTotalBytes;

       DWORD    cbAvailable;

       LPBYTE   lpbData;

       LPSTR    lpszContentType;

       BOOL (WINAPI * GetServerVariable) (HCONN hConn,

                                          LPSTR lpszVariableName,

                                          LPVOID lpvBuffer,

                                          LPDWORD lpdwSize);

       BOOL (WINAPI * WriteClient) (HCONN ConnID,

                                    LPVOID Buffer,

                                    LPDWORD lpdwBytes,

                                    DWORD dwReserved);

       BOOL (WINAPI * ReadClient) (HCONN ConnID,

                                   LPVOID lpvBuffer,

                                   LPDWORD lpdwSize);

       BOOL (WINAPI * ServerSupportFunction) (HCONN hConn,

                                              DWORD dwHSERequest,

                                              LPVOID lpvBuffer,

                                              LPDWORD lpdwSize,

                                              LPDWORD lpdwDataType);

    }EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;

Table 13.1. Extension Control Block structure definition.

    Type
    Name
    I/O
    Description
DWORD | cbSize | In | The size of the structure.
DWORD | dwVersion | In | The version number of the ISA specification that the ISA server implements. The major version number is in the HIWORD, and the minor version number is in the LOWORD. Extensions can use this value to gracefully degrade their functionality to older server versions' capabilities.
HCONN | connID | In | A number, referred to as a connection handle, that uniquely identifies the session. The handle is provided by the HTTP server; Microsoft says it should not be modified by an extension. Extensions will use this identifier as an argument to some server APIs.
DWORD | dwHttpStatusCode | Out | A value indicating the completion status of the requested operation.
CHAR | lpszLogData[HSE_LOG_BUFFER_LEN] | Out | A buffer containing a null-terminated string that will be posted to the ISA server log. This buffer is fixed at the size specified by the constant HSE_LOG_BUFFER_LEN. Extensions can post useful information in the HTTP server log file with this member.
LPSTR | lpszMethod | In | A buffer containing a string specifying the method used to make the request. This member corresponds to the REQUEST_METHOD variable in CGI. This is typically GET or POST.
LPSTR | lpszQueryString | In | A buffer containing a null-terminated string representing the command requested by the client. In typical CGI applications, such as search engines, this represents a query string, but is application specific and may be anything. This member corresponds to the QUERY_STRING variable in CGI.
LPSTR | lpszPathInfo | In | A buffer containing a null-terminated string representing a path in relation to the server's document root. This string typically identifies a particular document that should be returned to the client.
LPSTR | lpszPathTranslated | In | Similar to lpszPathInfo, this null-terminated string indicates the absolute server path to the document. The server generates this string.
DWORD | cbTotalBytes | In | The total number of bytes the extension has to receive from the client. This value corresponds to the CGI variable CONTENT_LENGTH. Further, if this value is 0xFFFFFFFF, there are at least (and maybe more than) four gigabytes of data to read. The extension should use the ReadClient API to fetch the data available.
DWORD | cbAvailable | In | The number of client bytes available for reading. This may be the same as cbTotalBytes, in which case all the data will be in lpbData (below.) If it's less than cbTotalBytes, the extension should call ReadClient until all the data is read.
LPBYTE | lpbData | In | The buffer of data from the client
LPSTR | lpszContentType | In | Indicates the content type of the data from the client. This null-terminated string corresponds to the CGI variable CONTENT_TYPE.

Four structure members are actually pointers to the ISA APIs you need to communicate with the server and the client. Extensions call these functions through the ECB. For example, a call to the ReadClient API might look like the following:

    pEcb->ReadClient(hConn, pBuffer, dwSize);

You can get other variables not represented in the ECB that CGI defines by using the ISAPI GetServerVariable. These variables can contain information about the connection or the implementation of the server. Unless your extension needs to perform some type of security authentication, you probably won't need anything more than what's in the ECB, so your example won't use this API. If you do need to perform authentication, use this API to access the AUTH_TYPE and REMOTE_USER variables. Refer to the appropriate CGI and HTTP specifications for details on how to implement secure extensions or for information on any other CGI-defined variables. This is the prototype for GetServerVariable:

    BOOL WINAPI GetServerVariable(HCONN hCOnn,

                                  LPSTR lpszVariableName,

                                  LPVOID lpvBuffer,

                                  LPDWORD lpdwSizeOfBuffer);

The HCONN parameter indicates the connection handle and is provided by the server as the ConnID member in the ECB structure. You supply a string containing the name of the variable you want in lpszVariableName, and the server copies it into a buffer you supply in lpvBuffer. You must tell the server the size of your buffer with the lpdwSizeOfBuffer parameter, and the server will reset the value of that parameter with the number of bytes actually copied. If the buffer isn't large enough, the call returns FALSE, and a call to GetLastError() indicates ERROR_INSUFFICIENT_BUFFER.

Your implementation of HttpExtensionProc is the main entry point for your extension and is called by the server when a client has a request. Extensions parse the data in the lpszQueryString member if the HTML <METHOD> tag is a GET operation. Alternatively, if the client requests a POST operation, the ReadClient API can get the data. This API is analogous to a CGI's use of the standard input stream and the CONTENT_LENGTH environment variable. For most client requests, a single call to ReadClient, such as

    BOOL ReadClient(HCONN hConn, LPVOID lpvBuffer, LPDWORD lpdwSize);

will get all the input, but the API supports multiple calls to fetch larger blocks.

Just as you do with GetServerVariable, you give the connection handle, a buffer to store the data in, and a value indicating the size of the buffer. If there is more data to fetch, GetLastError will indicate ERROR_INSUFFICIENT_BUFFER. An important thing to remember is that ReadClient will block and wait on the client until the amount of data you specify is available. Also, if the network socket has been closed, ReadClient returns TRUE, but the lpdwSize variable indicates that zero bytes have been read. An extension generally posts data back to the client in the form of HTML. In CGI, a script would indicate the format of the return data by writing a response header to the standard output. Instead, an extension uses the ServerSupportFunction API. The following line is the prototype of the function:

    BOOL ServerSupportFunction(HCONN hConn,

                   DWORD dwHSERequest,

                   LPVOID lpvBuffer,

                   LPDWORD lpdwSizeOfBuffer,

                   LPDWORD lpdwDataType);

The hConn argument is the connection handle, and the dwHSERequest argument tells the server what the extension wants it to do. Refer to Table 13.1 for a list of the possible values. Nearly all extensions use only the HSE_REQ_SEND_RESPONSE_HEADER command. The lpvBuffer argument changes meaning based on the type of server request you send. When using the HSE_REQ_SEND_RESPONSE_HEADER request, extensions put an optional status message to send back to the client, such as the ever-annoying but always useful 401 Access Denied message. If you set this argument to NULL, the server will handily send the message 200 OK for you. Use the lpdwSizeOfBuffer argument to indicate the size of the string in lpvBuffer.

Note that you should include the terminating NULL character at the end of the lpvBuffer string. The lpdwDataType argument is used to send a NULL-terminated string containing optional header information. This string nearly always contains the Content-type keyword to indicate the type of information you're sending back. The following shows a typical use of this API:

    TCHAR lpszBuff[] = "Content-type: text/html\r\n";

    DWORD dwSize = sizeof(lpszBuff);

    pEcb->ServerSupportFunction(hConn, HSE_REQ_SEND_RESPONSE_HEADER, NULL, &dwSize, (LPDWORD)lpszBuff);

Once the extension has determined what the client wants to do, either by getting the command string from QUERY_STRING or by using ReadClient, it performs the task it was asked to do and returns the results. It does this by forming HTML strings and using the WriteClient API. The following shows the prototype of this function, which is nearly identical to ReadClient:

    BOOL WINAPI WriteClient(HCONN ConnID, LPVOID Buffer, LPDWORD lpdwBytes, DWORD dwReserved);

After the connection handle, the argument Buffer is a pointer to a buffer containing the data to send to the client. For this example, you send NULL-terminated text data, but the type of data should match the Content-type string sent earlier. If the data in the buffer is a string, it should be null-terminated. The lpdwBytes argument tells the server how many bytes are in the buffer. This argument is also used by the server to indicate how many bytes were actually sent, which should always equal the number of bytes in the buffer. If the count of bytes is any amount less than that, then something bad happened, such as an interruption in the client network connection. Also, if you are sending back a NULL-terminated string, the variable should be set to the string length minus the NULL terminator.

###
REGBROWSE: A Server Directory Browser

I've harped on the fact that an extension is a DLL that runs in the server's address space. Since the server loads the extension either on demand or at startup, it must load the extension dynamically. To do this, the server executes the LoadLibrary Win32 function, specifying the module's name in the HTML document's URL.

Once the library is in memory, the server will use GetProcAddress, using the symbol names for the functions it needs to call—namely, GetExtensionVersion and HttpExtensionProc. This means you must export these functions by creating an export definition module (see Listing 13.5) with the names of these functions in the EXPORTS section. (There are other ways of exporting functions, but this is the most common way.) Most DLLs also export a default entry point called DllMain that's called by the operating system when LoadLibrary is executed; it gives you a convenient place to handle any initialization your extension needs, such as loading state information from disk or initializing global variables. DllMain is not strictly required, but Microsoft recommends that you create one. In your sample extension, however, you'll go against that recommendation because you have no global data or state information to load.

Listing 13.5. REGBROWSE.DEF: The Exports File.

    LIBRARY    regbrowse

    DESCRIPTION  'Internet Server Extension DLL'

    EXPORTS

        GetExtensionVersion

        HttpExtensionProc

I call this extension REGBROWSE, short for "A Regular Browser." The purpose of REGBROWSE is to allow clients to browse the server's file system. To that end, the extension has a very simple structure. You've already seen how GetExtensionVersion works, so there's no need to go over that again. Listing 13.6 shows REGBROWSE's implementation of HttpExtensionProc. Several variables are defined upfront to handle the directory browsing, and an object called CISHelper is declared locally, with a pointer to the ECB structure as an argument to the constructor.

The CISHelper class (see Listing 13.6) was created to help you parse incoming data and communicate with the server. It stores the ECB in its private member data to simplify its other calls. CISHelper's functionality is simple but useful—it handles reading commands from the client and sending data to it. It could also have handled the generation of HTML for you, but I chose to leave it in the main body. The INetSDK sample code contains a bonanza of APIs that make tasks like generating HTML much easier, and I leave it as an exercise to the reader to find and play with it. Once you've finished here, you'll have everything you need to know to understand the SDK's extension examples.

Listing 13.6. A partial listing of HttpExtensionProc.

    DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pEcb)

    {

        TCHAR   lpszDirectoryName[MAX_DIRECTORY_ENTRY+1];

        TCHAR   lpszDirectoryEntry[MAX_DIRECTORY_ENTRY];

        TCHAR   lpszDataBuffer[MAX_DIRECTORY_ENTRY];

        TCHAR   lpszFindString[MAX_DIRECTORY_ENTRY];

        CISHelper cHelper(pEcb);

        // Start the page

        cHelper.SendResponseHeader();

        cHelper.WriteClient(TEXT("<HEAD><TITLE>"));

        cHelper.WriteClient(TEXT("The Big Browser Page"));

        cHelper.WriteClient(TEXT("</TITLE></HEAD>\r\n\r\n"));

        cHelper.WriteClient(TEXT("<BODY>\r\n\r\n"));

        cHelper.WriteClient(TEXT("<h1>Server File System Browser</h1>"));

The first real piece of work HttpExtensionProc does is to send the response header back to the client. You'll recall that the response header informs the client about the extension output format. Once that business is taken care of, the extension begins HTML generation by sending the title of the generated page. It then calls the CISHelper method GetCommand. This method bears looking into a little deeper because of the ways it deals with the various input methods available to the client. (See Listing 13.7.)

GetCommand first determines what the input method is by checking the value of m_pEcb->lpszMethod for "GET". Since I created the HTML page that drives the extension, I know beyond a shadow of a doubt that you will get only GET or POST. If it's a GET, call the ConvertHtmlEscapes method (I'll get to that in a minute) and return the resulting string back to HttpExtensionProc. If it's a POST, check the value of m_pEcb->cbTotalBytes to see if no data was sent. If so, then HttpExtensionProc prints a nasty little message and returns it to the client.

Listing 13.7. Parsing the command string.

    BOOL CISHelper::GetCommand(LPTSTR lpszData, LPDWORD lpdwSize)

    {

        if(!m_pEcb)

        {

            return FALSE;

        }

        // It's a GET method. No URL escapes to process.

    // Just copy the string and get out.

        if (!stricmp(m_pEcb->lpszMethod, "GET"))

        {

            ConvertHtmlEscapes(lpszData, (char *)m_pEcb->lpszQueryString);

            // lstrcpy(lpszData, (char *)m_pEcb->lpszQueryString);

            *lpdwSize = lstrlen(lpszData);

            return TRUE;

        }

        else

        {

            if(m_pEcb->cbTotalBytes == 0)     // No query at all

            {

                *lpdwSize = 0;

                return FALSE;

            }

            else

            {

                DWORD dwCount = 0;

                char lpszTemp[1024];

                char *s = NULL;

                lstrcpy(lpszTemp, (char *)m_pEcb->lpbData);

                if(m_pEcb->cbTotalBytes - m_pEcb->cbAvailable > 0)

                {

                    m_pEcb->ReadClient(m_pEcb->ConnID,

                            (LPVOID) (lpszTemp + m_pEcb->cbAvailable),

                            &dwCount);

                }

                // Do escape char substitution

                s = strchr(lpszTemp, '=');

                if(!s)

                {

                    return FALSE;

                }

                ++s;

                ConvertHtmlEscapes(lpszData, s);

                *lpdwSize = strlen(lpszData);

            }

        }

        return TRUE;

    }

If there is data, m_pEcb->cbTotalBytes will be greater than zero and you handle two stages of input. In the first case, cbTotalBytes will be greater than cbAvailableBytes. If that happens, it means the server has given you only part of the data in the lpbData member variable, so you must call ReadClient to get the rest. If cbTotalBytes and cbAvailableBytes are the same, all the data the client ships is available in lpbData. In this example, you will probably never be called upon to exercise the ReadClient logic, but it's there just in case. Once you have the data, call ConvertHtmlEscapes again and return the string.

Now is a good time to talk about what ConvertHtmlEscapes does (refer to Listing 13.8). Windows directory paths contain at the very least two special characters (and possibly more that have special meaning to HTTP, so for you to get the data unscathed, it's delivered to the extension in a special format. The : and \ characters are replaced with the three ASCII characters &3A and &5C, respectively. Referring to your handy ASCII character set table, you see that these are the hexadecimal codes for these characters. You can't make much use of them this way, so you have to convert them to real hex numbers to build a directory search string later. Other characters are mangled as well, although they are easier to handle. For example, any spaces in the string are converted to a + character. ConvertHtmlEscapes takes as input two buffers, the destination and source strings. It scans through the source string looking for escape sequences and + characters and makes the appropriate conversions. It also dispenses with any carriage returns and line feeds. Once it returns, you have a string suitable for HttpExtensionProc's use.

Listing 13.8. Converting HTML escape sequences.

    void CISHelper::ConvertHtmlEscapes(LPSTR lpszDest, LPSTR lpszSource)

    {

        int i = 0;

        TCHAR *pChar = lpszSource;

        TCHAR *pDestChar = lpszDest;

        while(*pChar)

        {

            switch(*pChar)

            {

                case 0x0a:

                case 0x0d:

                    pChar++;

                    break;

                // The plus sign is really a space character

                case '+':

                    *pDestChar++ = ' ';

                    pChar++;

                    break;

                // Percent indicates a hex code. You need to

                // convert a two-byte ascii into an equivalent

                // hex number. For example, %3A == 0x3a

                case '%':

                    // skip over the percent sign

                    pChar++;

                    if (*pChar >= '0' && *pChar <= '9')

                    {

                        *pDestChar = *pChar - '0';

                    }

                    else

                    {

                        *pDestChar = *pChar + 0x0A - 'A';

                    }

                    // You've converted the high order byte;

                    // now shift it to its proper place

                    // in the output

                    *pDestChar <<= 4;

                    // Next!

                    pChar++;

                    if (*pChar >= '0' && *pChar <= '9')

                    {

                        *pDestChar |= *pChar - '0';

                    }

                    else

                    {

                        *pDestChar |= *pChar + 0x0A - 'A';

                    }

                    pChar++;

                    pDestChar++;

                    break;

                default:

                    *pDestChar++ = *pChar++;

                    break;

            }

            *pDestChar = '\0';

        }

    }

The next thing HttpExtensionProc does is to generate HTML representing the values contained in some of the more important ECB members. This will become useful to you when you start making modifications to the extension. Once you know what directory the user wants, concatenate the filter *.* to the end of the directory name and get a search handle by calling the Win32 function FindFirstFile. This function returns everything you ever wanted to know about a file, except whether it's a file or a directory. If it's a directory, you want to generate an HTML hyperlink so that the user can click on it to list its contents too. You do that by getting the attributes of the file and, if it's a directory, generating an HTML hyperlink that specifies a URL pointing to your script with the new directory appended to the string.

Refer to Listing 13.9 to see how the URL is generated. Note that the URL you generate results in a GET method. If it's a regular file, you print out the name, creation date, and file size as regular text. In both cases, you use the CISHelper method WriteClient to send text back to the server. Once you've finished listing the directory, generate an end-of-page HTML sequence and return a successful status to the server. Recall that your DLL remains in memory until the server decides there's no longer any need for it.

Listing 13.9. Generating a URL for a directory.

    // Convert the file's creation date so you can print it

            FileTimeToSystemTime(&stFindData.ftCreationTime, &stSysTime);

            dwFileAttr = GetFileAttributes(lpszDirectoryEntry);

            // if this is a directory, you want to create a bookmark

            // to allow the user to browse it. Otherwise,

            // you print the file data in plain text.

            if(dwFileAttr & FILE_ATTRIBUTE_DIRECTORY )

            {

                TCHAR lpszLine[MAX_DIRECTORY_ENTRY];

                wsprintf(lpszDataBuffer,

                         "http://%s/scripts/regbrows.dll?%s",

                         SERVERNAME,

                         lpszDirectoryEntry);

                wsprintf(lpszLine,

                        TEXT("<code><A HREF=\"%s\">%s</A><code><p>\r\n"),

                        lpszDataBuffer,

                        stFindData.cFileName);

                        cHelper.WriteClient(lpszLine);

            }

            else

            {

                wsprintf(lpszDataBuffer,

                "<code>%25s   %02d-%02d-%02d%d</code>\r\n",

                stFindData.cFileName,

                stSysTime.wMonth,

                stSysTime.wDay,

                stSysTime.wYear,

                stFindData.nFileSizeLow);

                cHelper.WriteClient(lpszDataBuffer);

                cHelper.WriteClient(TEXT("<p>\r\n"));

            }

Now take a look at the HTML that drives the extension. The salient features here are the two ways you can send requests to the server. The first one is a URL (see listing 13.10), which generates a request in the form of a GET method. I've hard-coded a directory path in the URL to give the user a starting point. The other is a FORM section that uses the POST method. The user can enter the desired starting directory and click the Submit button. Figure 13.1 shows a view of the REGBROWSE Web page user interface, and Figure 13.2 shows the results of the request.

Listing 13.10. HTML source for the REGBROWSE Web page interface.

    <h2>Select the default directory link below. </h2>

    <p>

    <A HREF="http://kevpc/scripts/regbrows.dll?c:\inetsrv\scripts">C:\InetSrv\Scripts</A>

    <FORM Action="http://kevpc/scripts/regbrows.dll" method=post>

    <p>

    Or enter a starting path in the field below.

    <p>

    <INPUT Name="Path" Value="c:\">

    <INPUT TYPE="SUBMIT" VALUE="Submit">

    </FORM>

Figure 13.1. The REGBROWSE Web page user interface.

Figure 13.2. A REGBROWSE-generated Web page.

####
Getting the Extension Running

To run the example, copy the DLL into your servers' scripts root. If you took all the defaults when you installed the IIS software, this will be in C:\inetsrv\scripts. You must also allow scripts to read and execute from the directory. (See Figure 13.3.) The Internet Service Manager applet allows you to choose the system account that clients will use when accessing server resources and what permissions they have. The very nature of this example requires more permissions than most applications require. You might want to set the client account to one that has permission to browse the directory tree. Doing this would cause most of the system administrators I know to have a fit of monstrous proportions, so make sure you don't install the example on a production machine.

Once you have the DLL in the scripts directory, make sure you make any needed modifications to the HTML file to point to the correct server. As shipped, it's hardcoded to use my development machine. (Don't even try to connect to it; you can't.) The code that generates URLs for directories also needs to be changed to point to your server. Once all that is done, use the Internet Service Manager applet to make sure the WWW service is running. You can then use your favorite browser to load up the HTML file and begin to play.

Figure 13.3. Setting server directory permissions.

###
Debugging ISA Extensions

Since the ISA server is the parent process of an ISA extension, it probably isn't immediately clear how one goes about setting up a debugging environment. The INetSDK does give you a pretty good answer to that problem, though it's not immediately apparent that it's there.

Under the ISAPI sample code tree, there's an innocuous little project called ISMoke. When you run this example, you see a dialog box where you enter your DLL extension name, a query string to send to the extension, and the method to use to send the request. (See Figure 13.4.) A checkbox allows you to quickly load and unload the DLL so that you can rebuild your project and run it again. I should also point out that, in the version of the SDK I have, ISMoke generates only GET methods. It always nulls out the cbBytesAvailable, cbTotalBytes, and lpvData members, so that even if you do send a POST, you'll get no data. Listing 13.11 shows a simple modification to the CISmokeDlg::Submit method in ismokedlg.cpp to make posts work.

CAUTION
By the time you read this, I expect Microsoft will have significantly polished its ISA development tools, but if they haven't, you've been warned!

Figure 13.4. The ISmoke extension debugging tool.

Listing 13.11. Getting ISmoke to support POST methods.

            ...

            ecb.lpszMethod = szMeth;

            // This is the modification to fix the post method

            if(stricmp(szMeth, "get") == 0)

            {

                ecb.lpszQueryString = szStmnt;

                ecb.cbTotalBytes = 0;

                ecb.cbAvailable = 0;

                ecb.lpbData = NULL;

            }

            else

            {

                ecb.cbTotalBytes = strlen(szStmnt)+1;

                ecb.cbAvailable = ecb.cbTotalBytes;

                ecb.lpbData = (unsigned char *)szStmnt;

            }

            ...

    }

As a final debugging alternative, you can make use of the lpszLogData member of the ECB structure to write debug information to the server log file. It might seem, in these days of high-powered graphical development tools, a little like grabbing a stone axe to hunt up some dinner, but you can also look at it this way: When you want raw performance, you don't buy a big plush car with leather seats.

###
Moving On

The techniques described here will help you produce some surprisingly powerful applications in a relatively short time. All that's needed is an extension DLL that exports the functions GetExtensionVersion and HttpExtensionProc, an HTML front-end that submits requests through either GET or POST methods, and a mechanism for generating HTML output for the client to view the results. Use the REGBROWSE server DLL implementation as a framework to implement applications, such as database query engines or data entry tools, commonly done today with client/server technology.

Of course, a much richer set of ActiveX tools than simple HTML is available for creating really jazzy client pages. In the next couple of chapters, you'll learn about 3-D views of information using Active VRML, online multimedia, and some practical development strategies. You shouldn't throw away your super-duper sockets class libraries just yet, but as you dig into ISAPI extensions and ActiveX controls, you'll soon find that those other fancy client/server tools are starting to gather dust. I expect to see a new crop of interesting and exciting Web server applications make their way onto the Internet. Until then, I'll be staring at that avocado seed.

| Use of this site is subject certain Terms & Conditions. Copyright (c) 1996-1999 EarthWeb, Inc.. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of EarthWeb is prohibited. Please read our privacy policy for details.