Program examples compiled using Visual C++ 6.0 compiler on Windows XP Pro machine with Service Pack 2 and some Figure screen snapshots have been taken on Windows 2000 server. The Internet Information Services version is IIS 4.x/5.x/6.x on Windows 2000 Server SP 4 and Windows XP Pro SP 2. The Internet Explorer is 6.x. Topics and sub topics for this tutorial are listed below. A complete information about IIS installation, configuration and testing a Web site is dumped HERE and how to setup FTP server also included HERE. Both Web and FTP servers were done on Windows 2000 Server SP4. Don’t forget to read Tenouk’s small disclaimer.
Index:ISAPI FiltersWriting an ISAPI Filter DLLThe MFC ISAPI Filter ClassesCHttpFilterCHttpFilterContextA Sample ISAPI Filter: myex34b.dll, myex34c.exeMYEX34B From Scratch: ISAPI FilterChoosing the NotificationSending Transaction Data to the Display ProgramMYEX34C From ScratchCMainFrame ClassCMyex34cApp ClassCMyex34cDoc ClassCMyex34cView ClassThe Story of The Display ProgramBuilding and Testing the MYEX34B ISAPI Filter |
An ISAPI server extension DLL is loaded the first time a client references it in a GET or POST request. An ISAPI filter DLL is loaded (based on a Registry entry) when the WWW service is started. The filter is then in the loop for all HTTP requests, so you can read and/or change any data that enters or leaves the server.
Writing an ISAPI Filter DLL
The ISAPI Extension Wizard makes writing filters as easy as writing server extensions. Choose Generate A Filter Object, and Step 2 looks something like this.

Figure 55: ISAPI Extension Wizard Step 2 of 2.
The list of options under Which Notifications Will Your Filter Process? refers to seven places where your filter can get control during the processing of an HTTP request. You check the boxes, and the wizard generates the code.
The MFC ISAPI Filter Classes
There are two MFC classes for ISAPI filters, CHttpFilter and CHttpFilterContext.
CHttpFilter
With the help of the ISAPI Extension Wizard, you derive a class from CHttpFilter for each ISAPI filter you create. There's just one object of this class. The class has virtual functions for each of seven notifications. The list of filters in the order in which IIS calls them is below.
virtual DWORD OnReadRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData);
virtual DWORD OnPreprocHeaders(CHttpFilterContext* pCtxt, PHTTP_FILTER_PREPROC_HEADERS pHeaderInfo);
virtual DWORD OnUrlMap(CHttpFilterContext* pCtxt, PHTTP_FILTER_URL_MAP pMapInfo);
virtual DWORD OnAuthentication(CHttpFilterContext* pCtxt, PHTTP_FILTER_AUTHENT pAuthent);
virtual DWORD OnSendRawData(CHttpFilterContext* pCtxt, PHTTP_FILTER_RAW_DATA pRawData);
virtual DWORD OnLog(CHttpFilterContext* pfc, PHTTP_FILTER_LOG pLog);
virtual DWORD OnEndOfNetSession(CHttpFilterContext* pCtxt);
If you override a function, you get control. It would be inefficient, however, if IIS made virtual function calls for every notification for each transaction. Another virtual function, GetFilterVersion(), is called once when the filter is loaded. The ISAPI Extension Wizard always overrides this function for you, and it sets flags in the function's pVer parameter, depending on which notifications you want. Here's a simplified sample with all the flags set:
BOOL CMyFilter::GetFilterVersion(PHTTP_FILTER_VERSION pVer)
{
CHttpFilter::GetFilterVersion(pVer);
pVer->dwFlags |= SF_NOTIFY_ORDER_LOW | SF_NOTIFY_NONSECURE_PORT |
SF_NOTIFY_LOG | SF_NOTIFY_AUTHENTICATION |
SF_NOTIFY_PREPROC_HEADERS | SF_NOTIFY_READ_RAW_DATA |
SF_NOTIFY_SEND_RAW_DATA | SF_NOTIFY_URL_MAP |
SF_NOTIFY_END_OF_NET_SESSION;
return TRUE;
}
If you had specified URL mapping requests only, the wizard would have set only the SF_NOTIFY_URL_MAP flag and it would have overridden only OnUrlMap(). IIS would not call the other virtual functions, even if they were overridden in your derived class.
CHttpFilterContext
An object of this second MFC class exists for each server transaction, and each of the notification functions gives you a pointer to that object. The CHttpFilterContext member functions you might call are GetServerVariable(), AddResponseHeaders(), and WriteClient().
A Sample ISAPI Filter: myex34b.dll, myex34c.exe
It was hard to come up with a cute application for ISAPI filters. The one we thought up, myex34b.dll, is a useful visual logging utility. IIS, of course, logs all transactions to a file (or database), but you must stop the server before you can see the log file entries. With this example, you have a real-time transaction viewer that you can customize.
MYEX34B From Scratch: ISAPI Filter
The following shows the steps to build MYEX34B, an ISAPI filter from scratch. Follow the shown steps.

Figure 56: MYEX34B – ISAPI Extension Wizard new project dialog.

Figure 57: MYEX34B – ISAPI Extension Wizard step 1 of 2, selecting Generate a Filter object option.

Figure 58: MYEX34B – ISAPI Extension Wizard step 2 of 2, selecting the notification priority, connection type and notification type options.

Figure 59: MYEX34B project summary.
Using ClassView, add the following two private member variables and a helper function.
HWND m_hWndDest;
HANDLE m_hProcessDest;
void SendTextToWindow(char* pchData);

Figure 60: Adding m_hWndDest, a member variable through ClassView.

Figure 61: Adding m_hProcessDest member variable.

Figure 62: Adding SendToWindow() member function.

Listing 14.
In myex34b.cpp, define the following constant.
#define WM_SENDTEXT WM_USER + 5

Listing 15.
Add the following code at the end but before the return statement of the GetFilterVersion().
...
...
// m_hWndDest, m_hProcessDest used by SendTextToWindow
m_hProcessDest = NULL;
if((m_hWndDest = ::FindWindow(NULL, "myex34c")) != NULL)
{
DWORD dwProcessId;
GetWindowThreadProcessId(m_hWndDest, &dwProcessId);
m_hProcessDest = OpenProcess(PROCESS_DUP_HANDLE, FALSE,
dwProcessId);
SendTextToWindow("MYEX34B filter started.\r\n");
}

Listing 16.
Edit the OnReadRawData() as shown below.
DWORD CMyex34bFilter::OnReadRawData(CHttpFilterContext* pCtxt,
PHTTP_FILTER_RAW_DATA pRawData)
{
// TODO: React to this notification accordingly and
// return the appropriate status code
TRACE("CMyex34bFilter::OnReadRawData\n");
// sends time/date, from IP, to IP, request data to a window
char pchVar[50] = "";
char pchOut[2000];
DWORD dwSize = 50;
BOOL bRet;
CString strGmt = CTime::GetCurrentTime().FormatGmt("%m/%d/%y %H:%M:%S GMT");
strcpy(pchOut, strGmt);
bRet = pCtxt->GetServerVariable("REMOTE_ADDR", pchVar, &dwSize);
if(bRet && dwSize > 1) {
strcat(pchOut, ", From ");
strcat(pchOut, pchVar);
}
bRet = pCtxt->GetServerVariable("SERVER_NAME", pchVar, &dwSize);
if(bRet && dwSize > 1) {
strcat(pchOut, ", To ");
strcat(pchOut, pchVar);
}
strcat(pchOut, "\r\n");
int nLength = strlen(pchOut);
// Raw data is not zero-terminated
strncat(pchOut, (const char*) pRawData->pvInData, pRawData->cbInData);
nLength += pRawData->cbInData;
pchOut[nLength] = '\0';
SendTextToWindow(pchOut);
return SF_STATUS_REQ_NEXT_NOTIFICATION;
}
Edit the helper function, SendTextToWindow() as shown below.
void CMyex34bFilter::SendTextToWindow(char *pchData)
{
if(m_hProcessDest != NULL) {
int nSize = strlen(pchData) + 1;
HANDLE hMMFReceiver;
HANDLE hMMF = ::CreateFileMapping((HANDLE) 0xFFFFFFFF, NULL,
PAGE_READWRITE, 0, nSize, NULL);
ASSERT(hMMF != NULL);
LPVOID lpvFile = ::MapViewOfFile(hMMF, FILE_MAP_WRITE, 0, 0, nSize);
ASSERT(lpvFile != NULL);
memcpy((char*) lpvFile, pchData, nSize);
::DuplicateHandle(::GetCurrentProcess(), hMMF, m_hProcessDest,
&hMMFReceiver, 0, FALSE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE);
::PostMessage(m_hWndDest, WM_SENDTEXT, (WPARAM) 0, (LPARAM) hMMFReceiver);
::UnmapViewOfFile(lpvFile);
}
}
Build MYEX34B, generating the myex34b.dll. We need another program, MYEX34C, to display the transaction data/messages. Before that let have some story about MYEX34B.
Choosing the Notification
Start by looking at the list of CHttpFilter virtual member functions. Observe the calling sequence and the parameters. For the MYEX34B logging application, we chose OnReadRawData() because it allowed full access to the incoming request and header text (from pRawData) and to the source and destination IP addresses (from pCtxt->GetServerVariable).
Sending Transaction Data to the Display Program
The ISAPI filter DLL can't display the transactions directly because it runs (as part of the IIS service process) on an invisible desktop. You need a separate program that displays text in a window, and you need a way to send data from the DLL to the display program. There are various ways to send the data across the process boundary. A conversation with Jeff Richter, the Windows guru who wrote Advanced Windows (Microsoft Press, 1997), led to the data being put in shared memory. Then a user-defined message, WM_SENDTEXT, is posted to the display program. These messages can queue up, so IIS can keep going independently of the display program.
We declared two handle data members in CMyex34bFilter::m_hProcessDest and CMyex34bFilter::m_hWndDest. We added code at the end of the GetFilterVersion() function to set these data members to the display program's process ID and main window handle. The code finds the display program's main window by its title (window’s title), myex34c, and then it gets the display program's process ID.
m_hProcessDest = NULL;
if((m_hWndDest = ::FindWindow(NULL, "myex34c")) != NULL)
{
DWORD dwProcessId;
GetWindowThreadProcessId(m_hWndDest, &dwProcessId);
m_hProcessDest = OpenProcess(PROCESS_DUP_HANDLE, FALSE, dwProcessId);
SendTextToWindow("MYEX34B filter started\r\n");
}
Below is a helper function, SendTextToWindow(), which sends the WM_SENDTEXT message to the display program.
void CMyex34bFilter::SendTextToWindow(char* pchData)
{
if(m_hProcessDest != NULL) {
int nSize = strlen(pchData) + 1;
HANDLE hMMFReceiver;
HANDLE hMMF = ::CreateFileMapping((HANDLE) 0xFFFFFFFF, NULL,
PAGE_READWRITE, 0, nSize, NULL);
ASSERT(hMMF != NULL);
LPVOID lpvFile = ::MapViewOfFile(hMMF, FILE_MAP_WRITE, 0, 0, nSize);
ASSERT(lpvFile != NULL);
memcpy((char*) lpvFile, pchData, nSize);
::DuplicateHandle(::GetCurrentProcess(), hMMF, m_hProcessDest,
&hMMFReceiver, 0, FALSE, DUPLICATE_SAME_ACCESS |
DUPLICATE_CLOSE_SOURCE);
::PostMessage(m_hWndDest, WM_SENDTEXT, (WPARAM) 0,
(LPARAM) hMMFReceiver);
::UnmapViewOfFile(lpvFile);
}
}
The DuplicateHandle() function makes a copy of MYEX34B's map handle, which it sends to the MYEX34C program in a message parameter. The MYEX34C process ID, determined in GetFilterVersion(), is necessary for the DuplicateHandle() call. Here is the filter's OnReadRawData() function, which calls SendTextToWindow():
DWORD CMyex34bFilter::OnReadRawData(CHttpFilterContext* pCtxt,
PHTTP_FILTER_RAW_DATA pRawData)
{
TRACE ("CMyex34bFilter::OnReadRawData\n");
// sends time/date, from IP, to IP, request data to a window
char pchVar[50] = "";
char pchOut[2000];
DWORD dwSize = 50;
BOOL bRet;
CString strGmt = CTime::GetCurrentTime().FormatGmt("%m/%d/%y %H:%M:%SGMT");
strcpy(pchOut, strGmt);
bRet = pCtxt->GetServerVariable("REMOTE_ADDR", pchVar, &dwSize);
if(bRet && dwSize > 1) {
strcat(pchOut, ", From ");
strcat(pchOut, pchVar);
}
bRet = pCtxt->GetServerVariable("SERVER_NAME", pchVar, &dwSize);
if(bRet && dwSize > 1) {
strcat(pchOut, ", To ");
strcat(pchOut, pchVar);
}
strcat(pchOut, "\r\n");
int nLength = strlen(pchOut);
// Raw data is not zero-terminated
strncat(pchOut, (const char*) pRawData->pvInData, pRawData->cbInData);
nLength += pRawData->cbInData;
pchOut[nLength] = '\0';
SendTextToWindow(pchOut);
return SF_STATUS_REQ_NEXT_NOTIFICATION;
}
MYEX34C From Scratch
The following are the steps to build MYEX34C. Follow the shown steps.

Figure 63: MYEX34C – new MFC AppWizard project dialog.

Figure 64: MYEX34C – Step 1 of 6, selecting SDI application.

Figure 65: MYEX34C – Step 2 of 6.

Figure 66: MYEX34C – Step 3 of 6, selecting Container in order to use CRichEditView class, deselect the Automation and ActiveX Controls.

Figure 67: MYEX34C – Step 4 of 6, selecting SDI application, no Docking toolbar feature.

Figure 68: MYEX34C – Step 5 of 6.

Figure 69: MYEX34C – Step 6 of 6, selecting CRichEditView for Doc and View as the base class.

Figure 70: MYEX34C – project summary.
Add the following context menu.
|
ID |
Caption |
|
IDR_MENUCONTEXT |
X |
|
ID_EDIT_CLEAR_ALL |
&Clear All |
|
Table 1. |
|

Figure 71: X menu property page.

Figure 72: Clear All menu item property page.
Just delete the View menu for the IDR_MAINFRAME, because we don’t need the Status bar.

Figure 73: Deleting the View menu.
CMainFrame Class
Define WM_SENDTEXT, a user define Windows message as shown below in MainFrm.h.
#define WM_SENDTEXT WM_USER + 5

Listing 17.
Delete the OnCreate(). Add OnSendText() message map function and OnUpdateFrameTitle() virtual function. Unfortunately the OnUpdateFrameTitle() function not available in ClassWizard of my Visual C++ 6.0, may be available in Visual Studio or VC++ with Service Pack. This function added manually.
virtual void OnUpdateFrameTitle(BOOL bAddToTitle);

Listing 18.
afx_msg LONG OnSendText(UINT wParam, LONG lParam);

Listing 19.
Add the following #include directive to MainFrm.cpp.
#include <afxpriv.h> // for AfxSetWindowText

Listing 20.
Add the message map manually.
ON_MESSAGE(WM_SENDTEXT, OnSendText)

Listing 21.
Just delete the OnCreate() and edit PreCreateWindow() as shown below.
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
return CFrameWnd::PreCreateWindow(cs);
}
Add the following message handlers’ implementation.
void CMainFrame::OnUpdateFrameTitle(BOOL bAddToTitle)
{
// the ISAPI filter needs to find this window by name
// just display the app name without the file name
TRACE("CFrameWnd::OnUpdateFrameTitle\n");
AfxSetWindowText(m_hWnd, AfxGetApp()->m_pszAppName);
}
LONG CMainFrame::OnSendText(UINT wParam, LONG lParam)
{
TRACE("CMainFrame::OnSendText\n");
LPVOID lpvFile = ::MapViewOfFile((HANDLE) lParam, FILE_MAP_READ, 0, 0,
0);
GetActiveView()->SendMessage(EM_SETSEL, (WPARAM) 999999, 1000000);
GetActiveView()->SendMessage(EM_REPLACESEL, (WPARAM) 0,
(LPARAM) lpvFile);
::UnmapViewOfFile(lpvFile);
::CloseHandle((HANDLE) lParam);
return 0;
}

Listing 22.
CMyex34cApp Class
Add the following code after the InitInstance().
AfxEnableControlContainer();

Listing 23.
Add the following code just after the AddDocTemplate().
// Enable DDE Execute open
EnableShellOpen();
RegisterShellFileTypes(TRUE);

Listing 24.
CMyex34cDoc Class
Using ClassWizard or ClassView, add SaveModified() virtual function.

Figure 74: Adding SaveModified() virtual function.
Then, edit the code as shown below.
BOOL CMyex34cDoc::SaveModified()
{
return TRUE;
}

Listing 25.
Change the following message map:
ON_UPDATE_COMMAND_UI_RANGE(ID_OLE_VERB_FIRST, ID_OLE_VERB_LAST, CRichEditDoc::OnUpdateObjectVerbMenu)
to
ON_UPDATE_COMMAND_UI(ID_OLE_VERB_FIRST, CRichEditDoc::OnUpdateObjectVerbMenu)

Listing 26.
CMyex34cView Class
Using ClassWizard or ClassView, add WM_RBUTTONDOWN windows message handler, also, command and command update for ID_EDIT_CLEAR_ALL.

Figure 75: Adding Windows message, command and update commands.

Listing 27.
Edit the code for the implementation part as shown below.
BOOL CMyex34cView::PreCreateWindow(CREATESTRUCT& cs)
{
BOOL bPreCreated = CRichEditView::PreCreateWindow(cs);
cs.style |= ES_READONLY;
return bPreCreated;
}

Listing 28.
void CMyex34cView::OnRButtonDown(UINT nFlags, CPoint point)
{
TRACE("CMyex34cView::OnRButtonDown\n");
CMenu menu;
menu.LoadMenu(IDR_MENUCONTEXT);
ClientToScreen(&point);
menu.GetSubMenu(0)->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,
point.x, point.y, this);
}
void CMyex34cView::OnEditClearAll()
{
CRichEditCtrl& re = GetRichEditCtrl();
re.SetSel(0, -1);
// won't let us clear unless we reset readonly flag
re.SetOptions(ECOOP_XOR, ECO_READONLY);
re.Clear();
re.SetOptions(ECOOP_SET, ECO_READONLY);
}
void CMyex34cView::OnUpdateEditClearAll(CCmdUI* pCmdUI)
{
pCmdUI->Enable(GetRichEditCtrl().GetTextLength() > 0);
}

Listing 29.
Build and run MYEX34C. The following is the output.

Figure 76: MYEX34C in action.
The Story of The Display Program
The display program, myex34c.exe, isn't very interesting. It's a standard AppWizard CRichEditView program with a WM_SENDTEXT handler in the main frame class:
LONG CMainFrame::OnSendText(UINT wParam, LONG lParam)
{
TRACE("CMainFrame::OnSendText\n");
LPVOID lpvFile = ::MapViewOfFile((HANDLE) lParam, FILE_MAP_READ, 0, 0,
0);
GetActiveView()->SendMessage(EM_SETSEL, (WPARAM) 999999, 1000000);
GetActiveView()->SendMessage(EM_REPLACESEL, (WPARAM) 0,
(LPARAM) lpvFile);
::UnmapViewOfFile(lpvFile);
::CloseHandle((HANDLE) lParam);
return 0;
}
This function just relays the text to the view. The MYEX34C CMainFrame class overrides OnUpdateFrameTitle() to eliminate the document name from the main window's title. This ensures that the DLL can find the MYEX34C window by name. The view class maps the WM_RBUTTONDOWN message to implement a context menu for erasing the view text. Apparently rich edit view windows don't support the WM_CONTEXTMENU message.
Building and Testing the MYEX34B ISAPI Filter
Build both the MYEX34B and MYEX34C projects, and then start the MYEX34C program. For IIS 1.0 – 3.0, to specify loading of your new filter DLL, you must manually update the Registry.
Run the Regedit/regedt32 application, and then double-click on Filter DLLs in \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W3SVC\Parameters. Add the full pathname of the DLL separated from other DLL names if any, with a comma.

Figure 77: Adding the full path of the DLL in registry – Windows XP Pro

Figure 78 Adding the full path of the DLL in registry – Windows 2000 Server.
To install an ISAPI filter for use with a Microsoft Internet Information Server version 4.0 - 6.0, 7.0 follow the following steps: