Introduction
Unless you have been a caveman, you have seen a number of web sites that whenever is browsed, the navigation ends up to a DLL file residing in a scripting directory of that dominium! Something like the following pseudo URL:
http://www.mydomain.com/script/example.dll?ID=p05874&Tx=870250AZT6
What does this DLL suppose to do and what does it have to do with today's paper?
These DLLs are created using the Internet Server API, or ISAPI for short. ISAPI is developed to provide a benefit or two, over the shortcomings of Common Gateway Interface, CGI. Although we surprisingly experience new web sites developed exclusively by CGI scripts nowadays, however, ISAPI DLLs have got something to offer that CGI could never bring us this way or that way.
I am going to start off this paper by describing the underlying details that any ISAPI programmer has to know, to be able to develop a better ISAPI extension. From then on, I will go through a development of a useful ISAPI extension step by step. The extension is supposed to validate a given credit card. Yeah! This is also my answer to those people who asked me the algorithm involved validating a credit card over and over again. Gotta go!
What is ISAPI?
Internet Server Application Programming Interface (ISAPI), is an API developed to provide the application developers with a powerful way to extend the functionality of Internet Information Server (IIS). Although ISAPI extensions by no means are limited to IIS, they are extensively used in conjunction with MS-IIS.
CGI vs. ISAPI
Developing a CGI program involves creating an EXE with C, C++, and/or Perl programming languages. This EXE file will be executed and terminated for every request received, causing an excessive memory usage, whenever users hit the same page over and over again!
This excessive memory usage that could bring the server completely down, has been solved under ISAPI extensions. An ISAPI extension is a regular DLL file that exposes 3 special functions that is called by the calling process (i.e., IIS) and therefore, will be loaded to memory once, no matter how many clients are going to use it at the same time. (It would be a good idea if you could take a look at a reference, to see how memory management is done under Windows 2000. The Visual C++ 6.0 Bible, Chapter 18, The Memory Management, describes it well!)
ISAPI fundamentals
Since the ISAPI extension and the calling process (IIS) live at the same address space, they could contact each other, directly. This means a great potential to bring the whole IIS down, and in some cases, the entire web server! Take a look at the following figure:
You see whatever problem your extension encounters, it could affect the entire web server process, if it's not handled properly. As illustrated above, communicating between the extension and IIS is done via a pointer to a structure of type ECB, or Extension Control Block that is declared as follows:
typedef struct _EXTENSION_CONTROL_BLOCK
{
DWORD cbSize;
DWORD dwVersion;
HCONN ConnID;
DWORD dwHttpStatusCode;
CHAR lpszLogData[HSE_LOG_BUFFER_LEN];
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;
Whatever information either the calling process or the extension wants to pass to the other, is done through this control block. We will shortly have a look at this ECB structure. For now, let's see how IIS works in conjunction with your extension, to serve the visitor of your web site.
Whenever an extension is accessed (e.g., http://www.mydomain.com/script/example.dll?ID=p05874 & Tx=870250AZT6), IIS checks to see whether the example.dll is loaded into memory. If it is not, then it initiates the loading process. Once the DLL is loaded into memory, a worker thread starts running to manage our extension, and thereafter the entry point (DLLMain
function) is called. When the DLLMain
finishes, the server makes a call to GetExtensionVersion
function to perform two tasks:
- Exchange version information
- Get a short text description of the extension
The server then calls the HttpExtensionProc
function passing a copy of the ECB's pointer to start the actual ISAPI extension. This is the function that makes writing data back to the client, possible! We will examine this, shortly.
The third and the last entry point in an ISAPI extension DLL is the TerminateExtension
function that is called whenever the extension is going to be unloaded from the memory. All the cleanup code can be done in this function.
In brief, an ISAPI extension is a regular DLL that exposes 3 functions to interact with the server:
GetExtensionVersion
HttpExtensionProc
TerminateExtension
(optional)
Having this information in hand, let's start with the DLLMain
, the entry point of any DLL!
DLLMain, the entry point
As indicated by Microsoft, "the DllMain
function is an optional entry point into a dynamic-link library (DLL). If the function is used, it is called by the system when processes and threads are initialized and terminated, or upon calls to the LoadLibrary
and FreeLibrary
functions", that is prototyped as follows:
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwCallReason,
LPVOID lpReserved);
If you provide your extension with this function, it will be called upon the initialization and the termination process of your extension. The state is indicated by the dwCallReason
parameter that could be one the following predefined values:
DLL_PROCESS_ATTACHED
DLL_THREAD_ATTACH
DLL_THREAD_DETACH
DLL_PROCESS_DETACH
Describing each of these parameters in detail is beyond the scope of this paper, so I simply refer you to Microsoft's Developer Network to read more about this function.
Anyhow, we could save the hModule
parameter for later use in our extension (if this suites us) and simply return TRUE
from this function. We usually do not have anything to do in this function, while developing an extension!
GetExtensionVersion, the actual entry point
This function is actually the first entry point that is called by IIS to determine the information about the extension. To understand this better, let's have a look at it's prototype:
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer);
Upon the activation of this function, we are supposed to fill out the extension information within the pVer
parameter passed to the function. This pointer is of type HSE_VERSION_INFO
that is declared as follows:
typedef struct _HSE_VERSION_INFO
{
DWORD dwExtensionVersion;
CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN];
}HSE_VERSION_INFO, *LPHSE_VERSION_INFO;
where dwExtensionVersion
is the extension version and lpszExtensionDescription
is the description of the extension. If we return TRUE
from this function, we notify IIS that our extension is ready to be used. Otherwise, IIS will not use the extension.
HttpExtensionProc, the main entry point
The amazing part of any ISAPI extension starts when the extension procedure (HttpExtensionProc
) is called. As far as you could remember, this is the procedure that makes writing data back to the client possible! To see how this happens, lets start by having a look at the prototype of HttpExtensionProc
:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);
where pECB
is a pointer to an extension control block, that makes the intercommunication between the server and the extension possible. You decide what the web page should contain and how to present it to the user in this entry point. But how?
Do you recall the members of the ECB's structure? ECB contains a method, prototyped as follows:
BOOL WriteClient(HCONN ConnID, LPVOID Buffer, LPDWORD lpdwBytes,
DWORD dwSync);
Using this member function, you can send the data present in the given Buffer
to the client that is identified by its ConnID
, the one that made the request. For example, to send A BIG RED ROSE to a client, you could simply make the following calls:
char szBigRedRos[] =
"<font color='#FF0000' size='3'><b>A BIG RED ROSE</b></font>";
DWORD dwSize = strlen(szBigRedRos);
pECB->WriteClient(pECB->ConnID, szBigRedRose, dwSize, 0);
Neat, huh? I think you already have got the minimum underlying knowledge to develop your first ISAPI extension. So, let's start...
Project requirements
- A little bit patience!
- MS-VC++ 6.0+ compiler
- MS-Windows 2000 Advanced Server with MS-IIS installed
- A web browser
Goals
We are going to develop a non-MFC ISAPI extension, using plain Win32 API calls that is supposed to validate a given Master Card. We name the extension validate.dll and it simply writes the client a string telling the client, whether the given credit card number is valid. Of course, we should examine what happens if a user wants to browse the page through the following URL:
http://mydomain/script/validate.dll?some%20string
or
http://mydomain/script/validate.dll?
or some sort of such invalid URLs, of course, from the purpose of our extension's view (these URLs are valid for your browser, though!).
In such circumstances, we simply echo the following text to the client: What you have entered is an invalid Master Card #.
And that's all what our extension does!
Why not MFC?
Although MFC amazingly simplifies the parsing process of query strings, it will dramatically increase the file size of our extension! On the other hand, if you know the subtle nuance of a non-MFC extension, you are a step further towards creating a better MFC extension. So, I decided to avoid using MFC for our first ISAPI extension.
Luhn algorithm
Now, the question is how can we check whether a given credit card number is valid. The algorithm involved in this checking is called Luhn algorithm (AKA Sum10 algorithm, AFAIK). Consider a number, say, 5168254236021548. To understand if this is a valid cc#, you should follow 4 easy guidelines:
- Starting from the leftmost digit, we simply multiply alternating numbers by two. i.e., we multiply shown bold numbers by two:
5168254236021548
5 * 2 = 10
6 * 2 = 12
2 * 2 = 4
4 * 2 = 8
3 * 2 = 6
0 * 2 = 0
1 * 2 = 2
4 * 2 = 8
- Add the separate digits of all the products, as follows:
1 + 0 + 1 + 2 + 4 + 8 + 6 + 0 + 2 + 8 = 32
- Add the unaffected digits to the product, i.e.,
1 + 8 + 5 + 2 + 6 + 2 + 5 + 8 = 37
- Add the result of the step 2 and 3 and divide it by 10
32 + 37 = 69
69 % 10 = 9
- If the answer is 0, this is a valid cc#, otherwise, it is not!
So 5168254236021548 is an invalid cc# (since 69 % 10 is 9 not 0).
Further checking
Having completed the Luhn checking, we could go a step forward by checking the type of the CC, according to the following table:
Credit Card |
Prefix |
Length (digits) |
Master Card |
51-55 |
16 |
VISA |
4 |
13, 16 |
American Express |
34, 37 |
15 |
However, for our example, we just assume that the given cc# is a Master card, so we check the card against the conditions for Master Cards, i.e., we could simply check if the given number has got 16 digits. If it's not, we simply echo that the give number is not a valid Master card number. You could extend the functionality, if you want.
How to implement Luhn checking in C?
Here, I've tried to program a procedure so that it simply returns 0 if the given string is a valid Master Card#. Otherwise, it will return a non-zero number indicating the error code:
#define ERR_WRONG_NUMBER_OF_DIGITS 1
#define ERR_NOT_A_MASTERCARD 2
#define ERR_INVALID_CC 3
#define ERR_INVALID_INPUT 4
BYTE CheckCC(const char *pszNumber)
{
if(strlen(pszNumber) != 16)
return ERR_WRONG_NUMBER_OF_DIGITS;
for(int i = 0; i < 16; i++)
if(!isdigit(pszNumber[i]))
return ERR_INVALID_INPUT;
if(pszNumber[0] != '5' || pszNumber[1] < '1' || pszNumber[1] > '5')
return ERR_NOT_A_MASTERCARD;
int nSum;
for(i = 0, nSum = 0; i < 16; i += 2)
{
int nDigit = (pszNumber[i] - 48) * 2;
nSum += (nDigit < 10 ? nDigit : nDigit / 10 + nDigit % 10)
+ (pszNumber[i + 1] - 48);
}
if(nSum % 10)
return ERR_INVALID_CC;
return 0;
}
And therefore, checking a master card is simply calling the following function:
BYTE byRet = CheckCC("1269875230210254");
if(!byRet)
{
}
else
{
}
Starting our extension
With this information in hand, it's now time to develop our ISAPI extension. Launch your VC++ compiler. From the File menu, select the New command. Having the Projects tab selected, left-click on Win32 Dynamic-Link Library. Within the Project Name space, type validate and click ok to continue. Within the Win32 Dynamic-Link Library box, select the second option, A simple DLL project and press the Finish button. At this point, we've got a DLL project ready to be implemented.
Since we are not interested either in ul_reason_for_call
or hModule
parameters, we skip them simply and we just return TRUE
in this entry point. So we have got the DllMain
function implemented as follows:
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
Now, let's gear from DllMain
to the first actual entry point, GetExtensionVersion
. As far as you could remember, we have to implement this function in order to serve two things described before. So let's add it to our project:
BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)
{
pVer->dwExtensionVersion = HSE_VERSION;
strncpy(pVer->lpszExtensionDesc,
"Validate ISAPI Extension", HSE_MAX_EXT_DLL_NAME_LEN);
return TRUE;
}
where HSE_VERSION
is defined in the httpext.h header file as follows:
#define HSE_VERSION MAKELONG(HSE_VERSION_MINOR, HSE_VERSION_MAJOR)
So add the mentioned header file to the top of the validate.cpp file. Pay a close attention, please! We return TRUE
to indicate that IIS is permitted to run our extension!
What's next, then? It's now time to find a way to get the query string back from IIS. But how? You probably remember that IIS can communicate with our DLL through the ECB's pointer, passed as the only parameter to the HttpExtensionProc
function:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);
where pECB
contains a data member, say, lpszQueryString
, that points to the query string. In other words, if the user tries to access our validate.dll via the http://mydomain.com/validate.dll?12345 URL, pECB->lpszQueryString
is equal to 12345, the string followed by the ? mark.
Another thing we should overcome before going on, is to learn how we could use pECB
to echo a string (including HTML, JavaScript and etc.) to the client. This is done via the WriteClient
method of the ECB block, which is prototyped as follows:
BOOL (WINAPI *WriteClient)(HCONN ConnID, LPVOID Buffer,
LPDWORD lpdwBytes, DWORD dwReserved);
where ConnID
is the connection identifier of the client to which the response data should be sent, Buffer
points to the data to be sent, lpdwBytes
is the length of the mentioned Buffer
, and dwReserved
as it sounds, is reserved.
With this information in hand, let's implement our HttpExtensionProc:
DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB)
{
StartContext(pECB);
BYTE byRet = CheckCC(pECB->lpszQueryString);
if(!byRet)
{
WriteContext(pECB,
"<p><b><font face='Verdana'
color='#008000'>Congratulations!</font>");
WriteContext(pECB,
"</b></p><p><font size='2' face='Verdana'>");
WriteContext(pECB,
"%s is a valid master card #</font></p>\r\n",
pECB->lpszQueryString);
}
else
{
WriteContext(pECB,
"<p><b><font face='Verdana'
color='#800000'>Sorry!</font></b></p>");
WriteContext(pECB,
"<p><font size='2' face='Verdana'>What you have entered is an ");
WriteContext(pECB,
"invalid master card#</font></p>\r\n");
}
EndContext(pECB);
return HSE_STATUS_SUCCESS;
}
And what about StartContext
, EndContext
, and WriteContext
functions? WriteContext
is a function to simplify the process of echoing a string to the client, implemented as follows:
void WriteContext(EXTENSION_CONTROL_BLOCK *pECB, char *pszFormat, ...)
{
char szBuffer[1024];
va_list arg_ptr;
va_start(arg_ptr, pszFormat);
vsprintf(szBuffer, pszFormat, arg_ptr);
va_end(arg_ptr);
DWORD dwSize = strlen(szBuffer);
pECB->WriteClient(pECB->ConnID, szBuffer, &dwSize, 0);
}
This way, we don't need to pass the length of the string or other non-useful information over and over again, while echoing a string. Moreover, we could use it like the MFC's CString::Format(...)
function, for example:
WriteContext(pECB, "5 + 6 = %d", 5 + 6);
that echoes the 5 + 6 = 11 string to the client.
On the other hand StartContext
is provided to echo necessary heading or startup HTML code to the client. It's counterpart, EndContext
, also echoes the footer to the client:
void StartContext(EXTENSION_CONTROL_BLOCK *pECB)
{
WriteContext(pECB, "<html>\r\n<body>\r\n");
}
void EndContext(EXTENSION_CONTROL_BLOCK *pECB)
{
WriteContext(pECB, "</body>\r\n</html>");
}
Now set the active project configuration to Release mode and compile the program. It is now time to see how to install and use our extension.
Assuming you are running Windows 2000, select the Internet Services Manager from the Administrative Tools of the Programs menu to bring the Internet Information Services snap-in to life. From the left panel, navigate your way through the available leaves of the web sites to the proper directory, where you plan to use the already created extension. This is usually done in scripts directory of that dominium. For this purpose, I've created a scripts directory under the articles folder:
Right click on the scripts directory and select properties to open up the following box:
Make the necessary changes to the box, so that it looks like the above picture and then apply the changes. Now, copy the validate.dll file from the release directory to the scripts directory you already configured. Launch your favorite web browser, and try to access the DLL, passing a number as its query string:
http://myth/articles/scripts/validate.dll?1234567890125436
Please pay attention that you have to change the above-mentioned URL to match the specific needs of your dominium.
Doing so, you'll see a damn familiar web page:
Oooooops! What went wrong, causing this damn page to appear?! If you think a little bit, you would probably remember that we have to expose those 3 (in our case 2) entry points we have used in our program, GetExtensionVersion
and HttpExtensionProc
. So let's go back to the project to fix things.
While in VC++ environment, create a blank file named validate.def and implement it this way:
; Validate.def : Declares the module parameters for the DLL.
LIBRARY "Validate"
DESCRIPTION 'Validate ISAPI Extension'
EXPORTS
; Explicit exports can go here
HttpExtensionProc @1
GetExtensionVersion @2
Then, from the project menu, select add to project, and then select the files item. Add the already created def file to the project. Rebuild the DLL. Let's copy the file from the release directory to the scripts directory again and browse the page again. This time, you will get the following page:
Now try to access the page using a valid master card number, and you'll get the congratulations page. Then try to delete the validate.dll located in the scripts directory! What happens? Yep! You'll get an error indicating that the DLL is already in use and cannot be deleted. So what we have to do, if we want to update our DLL? Shall we restart the computer, close the client, or ...? Nope! Not at all!
All you have to do is to stop IIS. This is done through the Internet Information Services snap-in! Launch it, and right click on the server's name. From the context menu, select restart IIS. In the "what do you want IIS to do?" combo box, select "stop Internet services on server" and press the ok button. Wait for seconds, and it's done! Now you can delete and/or modify your extension! Neat, huh?
The final word
This is what ISAPI extensions are supposed to do. They extend the functionality of IIS. Through this article, I repeated the word extension over and over again. It's now time to say that ISAPI programs are divided into two categories: ISAPI extensions and ISAPI filters.
ISAPI filters, unlike the ISAPI extensions would be called for any hit made to the web server! In other words, they magnificently slow down the process, since they are called over and over again. However, they could be absolutely useful when creating a logging service, or doing some specific jobs. Since describing ISAPI filters in detail deserves another paper, let me leave it here to you to understand the subtle nuances of how they work.
Anyhow, this was the simplest ISAPI extension that we developed today, just to show you what the heck an extension is and how it works. It was a synchronous, single-threaded DLL that is the most easiest DLL to develop! Real-world applications are not this easy to implement though, since you have to face the multi-threaded issues, as well as connection pools and other advanced topics. It's all up to you to learn how to play them magnificently, though, and this paper is just a starting point! That's all, folks. Aloha!
How to contact the author
I, as ever before, would love to hear your comments, questions and/or suggestions. So, please do not hesitate to send them to me, mehdi_mousavi@hotmail.com.
References
- Professional Visual C++ ISAPI Programming, by Michael Tracy
- Microsoft Developer Network, MSDN