Chapter 7

Creating ISAPI Extensions


Now we're ready to create ISAPI extensions. You already know that a 32-bit dynamic-link library (DLL) is the base for an ISAPI extension. So we'll create a 32-bit DLL project workspace in Visual C++ 4.x, define our export points, and show how extensions work.

Later, in Chapter 9, "Building Extensions with MFC," you'll learn to create ISAPI extensions using the MFC wrapper provided with Visual C++ version 4.1 and above.

You'll use the information you gain here as a basis for all the ISAPI extensions you create in C++.

Step 1: Create a 32-bit DLL

The 32-bit DLL project workspace holds a .CPP file, where your C++ code is stored, and a .DEF file, where your external declarations are defined. Visual C++ provides the make-file information for the DLL.

Visual C++ 4.x: Create a 32-bit DLL Workspace

To create a 32-bit project workspace in Visual C++ 4.x (Figure 7.1), follow these steps:

Fig. 7.1

Creating a project workspace.

  1. Open the Microsoft Developer Studio from the program manager or Start menu in Windows 95 or NT 4.0.
  2. Choose File, New, and the select Project Workspace from the New dialog and choose OK.
  3. When the New Project Workspace dialog appears, enter the name of your project in the Name text field.
  4. Select Dynamic Link Library from the Type list and then select Create.
  5. Choose Insert, File into Project and type the name of the .CPP file that you are creating for your extension. Since the file does not exist, Visual C++ asks you if you want to insert it anyway. Answer Yes.
  6. Choose Insert, File into Project, and type the name of the .DEF file that you are creating for your Extension. (Usually the same name as your CPP file only with a.DEF extension). Once again, the file does not exist. Answer Yes to the dialog box asking if you want to insert it anyway.
  7. As a final step, open your CPP file by first double-clicking the Project Workspace folder and then double-clicking the file name. Visual C++ prompts you to create the file. Answer Yes.

Fig. 7.2

Opening the .CPP file.


[Note]


Entry Points


Don't forget to define your entry points. You do this in the .DEF file. Open your .DEF file by opening the Project Workspace folder and double- clicking on the file name. Visual C++ prompts you to create the file. You need to answer Yes. Here, you enter your entry-point information.


LIBRARY HelloWorld


EXPORTS GetExtensionVersion


HttpExtensionProc


DllMain


In version 2.0 of ISAPI, you can expose an optional export function called TerminateExtension.


BOOL WINAPI TerminateExtension( DWORD dwFlags);


This provides a safe way to clean up any memory or free any resources you may have allocated.


Besides including WINDOWS.H in your extension, you also need to include httpext.h, which is part of the Win32 SDK or available in any version of Visual C++ after 4.0 (i.e..1 or 4.2).

#include <windows.h>

#include <httpext.h>


[Tip]


To save some compile time, you may want to define WIN32_LEAN_AND_MEAN before you include WINDOWS.H. This exempts parts of the windows.h file like sound and video support, and gives you a faster compile.


Add DllMain()for Startup/Shutdown Processing

The first function we'll look at is DllMain. With simple ISAPI extensions, DllMain does not need to be more complicated than the example in Listing 7.1 DLLMAIN.CPP-DllMain.initialized in the DllMain

Listing 7.1 DLLMAIN.CPP-DllMain

BOOL WINAPI DllMain (HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpv)

{

return TRUE;

}

Step 2: Define GetExtensionVersion()

ISAPI servers need to know what version of the ISAPI specification your extension complies with. You getorder to acquire this information with, the first call after DllMain made to your extension: the GetExtensionVersion entry point. Let's look at what happensis accomplished in GetExtensionVersion.

The GetExtensionVersion Function Prototype

You know from the previous chapter that all ISAPI extensions need two standard entry points. The first is the GetExtensionVersion entry point.

BOOL WINAPI GetExtensionVersion (HSE_VERSION_INFO *pVersion)

You use this mandatory entry point to allow your ISAPI extension to load on the Web server. The GetExtensionVersion entry point passes a pointer to the HSE_VERSION_INFO structure.

typedef struct _HSE_VERSION_INFO {

DWORD dwExtensionVersion;

CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN];

} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;


[Tip]


If you are using global variables, they should be initialized in the GetExtensionVersion function. If you need to allocate or reserve any system resources or read registry settings, GetExtensionVersion may be the best place to do this.


Set the ISAPI Version Number

The first task in the GetExtensionVersion entry point is to set the ISAPI version number. HTTPEXT.H defines the ISAPI version number. The dwExtensionVersion member of HSE_VERSION_INFO needs to be set to it. You set this value with the following line of code:

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

When the server first loads your ISAPI extension, it executes the GetExtensionVersion entry point and evaluates the return of the HSE_VERSION_INFO structure. The ISAPI version you set here tells the server what type of functionality to expect.

This information tells the server what to send to the extension procedure as server parameters and extended browser information. In the future, ISAPI could offer extended information that the current implementation of ISAPI does not allow.

It is always important for the ISAPI server to know what to send to the extension and what to hide from the extension.

Set the ISAPI Extension Description

The next information for the server is a description of your ISAPI extension.

The lpszExtensionDesc member of HSE_VERSION_INFO points to the description string for your ISAPI extension. By using the lstrcpyn function to set your description, you won't exceed the maximum length allowed. HSE_MAX_EXT_DLL_NAME_LEN holds the maximum length of the DLL description.

lstrcpyn(pVersion->lpszExtensionDesc, "Hello, World", HSE_MAX_EXT_DLL_NAME_LEN);


[Tip]


Keep your extension description descriptive. Use the full name of your program, with spaces, in the description. Remember, the server may provide this information in its administration tools.


Once this description has been set, the server's administrative tools may display it. This enables the server administrator to see what ISAPI extensions are running or have been run.

You can also do additional DLL initialization in GetExtensionVersion. You are guaranteed that this entry point is called only once.


[Note]


GetExtension version is called with the system-user context, allowing you to read from the system registry without impersonating another user. If you need to read registry settings from within HTTPExtensionProc, you'll need to impersonate a user with the proper access, using ImpersonateLoggedOnUser.


Return TRUE If Succeeded, FALSE If Failed

The final step to GetExtensionVersion is to return a value. If you have initialized something that your DLL needs and the initialization failed, you can return FALSE. This tells the server that it should not to proceed with calling HttpExtensionProc. Typically, you should be returning a value of TRUE.

A bare-bones GetExtensionVersion function is shown in Listing 7.2 GetExtensionVersion.CPP-GetExtensionVersion.

Listing 7.2 GetExtensionVersion.CPP-GetExtensionVersion

BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVersion)

{

pVersion->dwExtensionVersion = MAKELONG(HSE_VERSION_MINOR,

HSE_VERSION_MAJOR);

lstrcpyn(pVersion->lpszExtensionDesc,

"Hello World Extension 1.1",

HSE_MAX_EXT_DLL_NAME_LEN);

return TRUE;

}

Step 3: Define HttpExtensionProc()

HttpExtensionProc may be considered the "main" function of the program. This is where the heart of the extension is. And tThis is where you wihandle all the requests that come to your extension.

Use the Mandatory Function Prototype

HttpExtensionProc is the second mandatory entry point. You need to use the mandatory function prototype for it.

DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)

HttpExtensionProc is passed in a pointer to the Extension Control Block (ECB) structure. The ECB gives you all the information you need to process a request.

You'll use the ECB to execute functionality exposed by the server, and to get information from the Web client and post information back to it. Many of the variables in the structure are like variables defined in the environment of a CGI application.

Using the ECB

Table 7.1 The ECB Member Variables

Member

Type

CGI Equivalent

Description

cbSize

DWORD

none

This member contains the size of the ECB structure.

dwVersion

DWORD

none

This contains the version of the ISAPI specification.

ConnID

HCONN

none

This is a handle to the connection ID. The connection ID is assigned by the server and certain functions, like WriteClient, need it passed to understand which connection to send data to. This number should not be changed.

dwHttpStatusCode

DWORD

once you have completed your HttpExtensionProc function you must set this value, or return a value that is a valid status code. In most cases HS_STATUS_SUCCESS.

This is the status of the current transaction when the request is completed.

lpszLogDataHSE_LOG_BUFFER_LEN]

CHAR

none

This is a null-terminated string. This string contains extended log information for the server log file. What you put here will show up in the server log file and should offer a description of what the user did in your extension or any error conditions.

lpszMethod

LPSTR

Request_Method

This contains the method with which the HTTP request was made. that data was sent to your form. This usually contains POST or GET.

lpszQueryString

LPSTR

Query_String

This is a null-terminated string containing the encoded query information from the client.

lpszPathInfo

LPSTR

Path_Info

PathInfo contains extra path information.

lpszPathTranslated

LPSTR

Path_Translated

Path translated contains the physical path of the ISAPI DLL.

cbTotalBytes

DWORD

Content_Length

Total Bytes contains the total amount of bytes returned by the client. This information is used when receiving data from the client from a POST request. If the value is 0xfffffffff then there are 4 Gbytes available and it should be read using ReadClient until no additional data is returned.

cbAvailable

DWORD

none

This contains the total available bytes already read into the lpbData buffer. The difference between cbAVailable and cbTotalBytes needs to be read in using the ReadClient member function.

lpbData

LPBYTE

none

This contains some or all of the data sent by the client. It is a buffer the size of cbAvailable. maximum size of this buffer is 48k. If there is more information from the client, cbTotalBytes will contain the total size of the data block being posted back to the server by the client and you must use ReadClient to retrieve it.

lpszContentType

LPSTR

Content_Type

This contains the content type of data sent from the client. This could be any legal MIME type.

Using the ServerSupportFunction and WriteClient to Return a Page to the Client

Before you can send any information to the client, you should send a hypertext transport protocol (HTTP) response header. In this header, you can send special commands to the Web browser, like the redirect command, or you can send information about the type of content you want to return. In most cases you are informing the browser what content you will be sending.

The content information the browser gets in the header must be in a strict format. Always start with Content-type: and follow with a MIME type of what type of data you will be sending.

In the tutorial at the end of this chapter, we create a function that simplifies writing the header. To write a header, you need to use ServerSupportFunction, a member of the ECB structure.

BOOL WINAPI ServerSupportFunction(HCONN hConn., DWORD dwHSERRequest, LPVOID lpvBuffer, LPDWORD lpdwSize, LPDWORD lpdwDataType);

The MIME type we need to use for any Hypertext Markup Language (HTML) we are returning is text/html. So our first step in returning data in our HttpExtensionProc is to send the HTTP header.


[Tip]


The header you are returning here is very precise. Case sensitivity matters in most cases and the end of the response header is critical. You must return an \r\n at the end of the header or the client will not know what to do.


Content-type: text/html\r\n

DWORD dwWritten;

LPTSTR header = new TCHAR[200];

lstrcpy(header,"Content-type: text/html\r\n");

dwWritten = sizeof(header);

pECB->ServerSupportFunction(pECB->ConnID,

HSE_REQ_SEND_RESPONSE_HEADER,

NULL,

&dwWritten,

(LPDWORD) header);

Once we have told the client or browser what we are about to send, we can send the actual content.

To send information to the client, you use the WriteClient function, a member of the ECB. The WriteClient function updates a buffer that the server will be sending to the client.

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

As with ServerSupportFunction, we need to pass a connection ID to the WriteClient function. The connection ID is a member of ECB. If you combine the following code with the previous example of ServerSupportFunction, you will write "Hello, World..." to the browser.

strcpy(outputstring,"Hello, World...");

dwBytesWritten=lstrlen(outputstring);

pECB->WriteClient(pECB->ConnID, (PVOID) outputstring, &dwBytesdWritten,0);

You can also use wWriteClient to write binary data such as images to the client.

Once you have completed the tasks in HttpExtensionProc, you need to return the status of your command to the server.

Return the Appropriate Status Code

HSE_STATUS_SUCCESS is returned when your application has finished processing. When this is returned, the server disconnects from the client and frees allocated resources.

You can return a total of four different status codes to the client. These status codes are listed in Ttable 7.2.

Table 7.2 Status Codes

Status Code

Description

HSE_STATUS_SUCCESS

Your extension has successfully completed and the server can continue with the disconnect of the client.

HSE_STATUS_SUCCESS_AND_KEEP_CONN

Your extension has completed and instructs the server to wait for the next request for the client if the client supports persistent connections. The server is not required to keep the communication open order to ensure a persistent connection you need also to send a proper HTTP header to the client.

HSE_STATUS_PENDING

Your extension has not completed executing and will notify the server when it is finished.

HSE_STATUS_ERROR

Your extension has not completed properly. The server is instructed to disconnect the client.

Tutorial 2:Building "Hello, World!"

Earlier, we built a simple "Hello, World!" ISAPI extension experimenting with it. Now we will build another ISAPI extension, build based on the previous instructions, and customize it.

First create a 32-bit project workspace, as instructed earlier in this chapter. Then add the DllMain function and the mandatory prototypes HttpExtensionProc and GetExtensionVersion.

We use two functions to simplify our extension. These are WriteHTML and WriteHTMLHeader. We can use these to make calling the WriteClient and ServerSupportFunction functions a little easier.

Listing 7.3 WriteFunctions.CPP-WriteHTML and WriteHTMLHeader

BOOL WriteHTML(EXTENSION_CONTROL_BLOCK *pECB,

TCHAR &OutputString)

{

DWORD dwBytes;

dwBytes = lstrlen(OutputString);

return pECB->WriteClient(pECB->ConnID,

(PVOID) OutputString,

&dwBytes,

0);

}

BOOL WriteHTMLHeader(EXTENSION_CONTROL_BLOCK *pECB)

{

DWORD dwSize;

TCHAR str[] = TEXT("Content-type: text/html\r\n");

dwSzie = sizeof(str);

return pECB->ServerSupportFunction(pECB->ConnID,

HSE_REQ_SEND_RESPONSE_HEADER,

NULL,

&dwSize,

(LPDWORD) str);

}

Examining GetExtensionVersion

The "Hello, World!" GetExtensionVersion is as simple as the one at the beginning of this chapter. We set the ISAPI version, copy the description into the description variable, and return a value of TRUE for success.

Examining HttpExtensionProc

Now that we have written the two utility functions WriteHTML and WriteHTMLHeader, our HttpExtensionProc is pretty bare-bones. Our first step is to send the content-type header back to the client using the function we wrote, WriteHTMLHeader.

WriteHTMLHeader does a ServerSupportFunction that sends a response header of Content-type: text/html\r\n, which lets the client know we are sending HTML. The function gets a pointer to the ECB, which it uses to make its calls.

WriteHTML simplifies the WriteClient function by determining the size of the string we are sending and passing it to the WriteClient function. This works well for sending text strings.


[Tip]


You can use WriteClient and ServerSupportFunction to send any kind of data back to the client that the client understands. You may want to dynamically generate a chart in a gif image and send it back to the client.


You then have to set the content-type header to image/gif and send the gif binary data with the WriteClient function.


Once "Hello, World!" has written out its information to the client, the final step is to tell the server it has completed its task by returning a HSE_STATUS_SUCCESS.


[Tip]


An easy way for you to test your extension without having to load it into the server is by using EyeSAPI. EyeSAPI loads and logs your DLL, executes it, and lets you view the results in any browser.


When writing the "Hello, World!" extension, I used EyeSAPI rather than my sever to do most of the testing. EyeSAPI is available on the CD that comes with this book and from http://rampages.onramp.net/~steveg/eyesapi1.zip.


Adding Dynamic Data

Let's give the "Hello, World!" extension some life. We use the GetLocalTime API call to retrieve the current date and time, and send it to the client. We can also enhance the "Hello, World!" extension with some HTML.

Change HttpExtensionProc to look like the following:

TCHAR temp[200];

SYSTEMTIME CurrentTime;

WriteHTMLHeader(pECB);

WriteHTML(pECB,"<html><head><title>");

WriteHTML(pECB,"Hello, World");

WriteHTML(pECB,"</title></head><body bgcolor=#ffffff>");

WriteHTML(pECB,"<p align=center>");

GetLocalTime(&CurrentTime);

sprintf(temp,"Hello, World. It is currently %02d:%02d on %02d/%02d/%02d.",

CurrentTime.wHour,

CurrentTime.wMinute,

CurrentTime.wMonth,

CurrentTime.wDay,

CurrentTime.wYear);

WriteHTML(pECB,temp);

WriteHTML(pECB,"</p></body></html>");

Now rebuild the DLL and execute it in your browser. The results are shown in Figure 7.3.

Fig. 7.3

Results of "Hello, World!"

From Here...

In this chapter, we looked at how to build a very simple ISAPI extension. We discussed the mandatory entry points for ISAPI extension DLLs-GetExtensionVersion and HttpExtensionProc.

In the next chapters, we'll look at building more complex extensions and, how to use the MFC. ISAPI classes Finally, and finally we'll build some extensions you can use, either with or without changemodification toon your Web server.


© 1996, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.