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.
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 servera 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 InterfaceCGIand it's really not magic at all. Like browsers and HTML, it's
surprisingly simple. Web serverbased 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.
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.
<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.
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.
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:
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.
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.
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.
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.
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;
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.
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 callnamely, 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.
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 usefulit 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.
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.
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.
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.
// 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.
<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.
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.
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.
Figure 13.4. The ISmoke extension debugging tool.
... 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.
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.