The two examples in this chapter should give you a feel for the capabilities of an Internet server application programming interface (ISAPI) extension. The first example introduces you to a comment entry form and shows you how to parse data sent by the client to the extension.
The second example prints out information on how you called the extension. This shows you how the extension has affected Microsoft's Internet Information Server (IIS) variables.
When a form uses the GET method to pass information to the extension, it is called like this:
http://www.fred.com/extension.dll?param1=value1¶m2=value2
When the extension is executed, it reads the information either by reading the server variable QUERY_STRING or by looking at pECB->lpszQueryString. Working with pECB->lpszQueryString is the preferred way of reading the data because you don't have to call any functions to receive the data.
The snippet of code in Listing 8.1 shows how you can tell that GET is the request method used.
Listing 8.1 IISEMAIL.CPP-How to Determine a GET Request Method
DWORD
HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB )
{
CHAR *lpszQuery = NULL;
// get the data from the form
if( 0 == stricmp(pECB->lpszMethod,
"get") )
lpszQuery = pECB->lpszQueryString;
....................
}
But you still have some work to do before you can use the query string.
Contents of lpszQueryString
lpszQueryString is the equivalent of the server variable QUERY_STRING (explained later in this chapter). Like the server variable, lpszQueryString is encoded into name/value pairs.
Fig. 8.1Comment entry page.
If you take the HTML form shown in Figure 8.1 as an example, you end up with a query string like this,
FirstName=Martin&LastName=Norman&WebUse=Web+Surfer&FromEMail=&HomePage=http%3A%2F%2Fstaff.ndl.co.uk%2Fmartin&text=
in which FirstName=Martin is a name/value pair.
Parsing URL-Encoded Parameters into Name/Value Pairs
Now that you have the query string, you have to parse it to turn it into something meaningful.
In this example, we first extract the names in the form. These are:
The name is separated from the value by the '=' character: for example, FirstName=Martin. So first you scan the query string for an occurrence of FirstName. If you take the '=' character into account, you have the start of the value.
The end of the value is either the "&"character or the end of the buffer. The function shown in Listing 8.2 returns the value part of the name/value pair.
Listing 8.2 PARSE.CPP-Getting One Name/Value Pair Out of the Query String
CHAR * CIissmtpExtension::
GetParamValue(CHAR *lpszQuery, CHAR *lpszParam)
{
CHAR *pValueStart = NULL;
CHAR *pValueEnd = NULL;
CHAR *lpszValue = NULL;
CHAR *szTemp1 = NULL;
ULONG cbValue;
pValueStart = strstr( lpszQuery, lpszParam );
if( !pValueStart ) // parameter doesn't exist
return NULL;
pValueStart += strlen( lpszParam ) + 1;
// Now determine the length of the value string.
pValueEnd = strchr(pValueStart, '&');
if(pValueEnd)
cbValue = pValueEnd - pValueStart;
else
// this was the last param in the list
cbValue = strlen(pValueStart);
// Return NULL if we have zero length string.
if( !cbValue )
return NULL;
if( !(lpszValue = (CHAR *)LocalAlloc(LPTR, cbValue + 1) ) )
return NULL;
strncat(lpszValue, pValueStart, cbValue);
szTemp1 = lpszValue;
while( * szTemp1 )
{
if( *szTemp1 == '+' )
*szTemp1 = ' ';
szTemp1++;
}
EscapeToAscii(lpszValue);
return lpszValue;
} // GetParamValue
In the query string example above, notice that the HomePage parameter
has a lot of odd characters in it:
HomePage=http%3A%2F%2Fstaff.ndl.co.uk%2Fmartin
Some characters are reserved and can't be used. So the client converts them to hex values, denoted by the '%' character. All spaces are converted to the '+' character.
In Listing 8.2, you saw a reference to a function called EscapeToAscii. This function does the conversion from hex values to ASCII characters for you (see Listing 8.3).
Listing 8.3 PARSE.CPP-Converting a Hex Value into a Character
void CIissmtpExtension::
EscapeToAscii(CHAR *lpEscape)
{
int i, j;
for( i = 0, j = 0; lpEscape[j] ; ++i, ++j )
{
if( (lpEscape[i] = lpEscape[j]) == '%' )
{
lpEscape[i] = HexToAscii( &lpEscape[j+1] );
j+=2;
}
}
lpEscape[i] = '\0';
}
//----------------------------------------------------------------------
CHAR CIissmtpExtension::
HexToAscii(CHAR *lpString)
{
CHAR CH;
CH = (lpString[0] >= 'A' ? ( (lpString[0] & 0xDF) - 'A' ) + 10 : (lpString[0] - '0') );
CH *= 16;
CH += (lpString[1] >= 'A' ? ( (lpString[1] & 0xDF) - 'A' ) + 10 : (lpString[1] - '0') );
return CH;
}
Now you have converted your query string into something you can use.
Reading data that was sent using the POST request method is slightly more complex than reading data that was sent using the GET method. But it's worth it. You can read considerably more data from the client using the POST method and that data can be binary.
Listing 8.4 IISEMAIL.CPP-How to Determine a Post Method
DWORD
HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB )
{
CHAR *lpszQuery = NULL;
// get the data from the form
if( 0 == stricmp(pECB->lpszMethod,
"post") )
{
// extract post data here
....................
....................
....................
....................
}
....................
....................
....................
....................
}
The first 48 kbytes of data (if you have that much) from the POST request are pointed to by the EXTENSION_CONTROL_BLOCK pointer lpbData, with the total amount of data available shown by cbTotalBytes.
This 48 kBytes is a limitation of Microsoft's IIS. Different servers may have a different value. In version 3.0 of Microsoft's IIS, the value is configurable. The default is still 48 kBytes. To change the default, you have to change the following registry value under HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\w3svc\parameters\UploadReadAhead
The value range for this parameter is 0 - 0x80000000![]()
The maximum amount of data you can send using this request method is 4 Gbytes. Sending the full amount of data is covered later in this chapter.
There is no end-of-file (EOF) at the end of the lpbData buffer.
![]()
If the data is not binary, it is URL-encoded just like the GET method. So you have to parse the data. Then you can decode the data.
Difference Between cbTotalBytes and cbAvailable
The content of cbTotalBytes is the equivalent of the server variable CONTENT_LENGTH (explained later in this chapter). If its value is 0xffffffff, then the connecting client has just sent the over 4 Gbytes of data to the extension.
The contents of cbAvailable are the number of bytes immediately available (out of a total of cbTotalBytes). To read the remaining data, you use the ReadClient function, which is covered in the next section.
Both of these values refer to the content of the lpbData buffer. The maximum amount of data this can hold depends on the implementation of the ISAPI interface you are using. In Microsoft's IIS, the default value is 48 kbytes. But this value can be changed using the registry.
Getting the Full POST Buffer
Read the data to check if cbAvailable is equal to cbTotalBytes. If it is, just make a copy of the contents of lpbData. If cbAvailable is less than cbTotalBytes, then use ReadClient() until all the data has been read.
Listing 8.5 IISEMAIL.CPP-How to Get the Whole POST Buffer
// Start processing of input information
here.
lpszTemp = (CHAR *)LocalAlloc( LPTR, pECB->cbTotalBytes);
if( NULL == lpszTemp )
return HSE_STATUS_ERROR;
// do not always trust the compiler to
zero the memory
memset(lpszTemp, '\0', pECB->cbTotalBytes);
strncpy(lpszTemp, (CHAR *)pECB->lpbData,
pECB->cbAvailable );
cbQuery = pECB->cbTotalBytes - pECB->cbAvailable;
if( cbQuery > 0 )
{
pECB->ReadClient(pECB->ConnID,
(LPVOID)(lpszTemp + pECB->cbAvailable),
&cbQuery);
}
lpszQuery = lpszTemp;
Using lpszContentType
The content type entry shows the type of data sent by the client. It is only used when the request method is the POST.
lpszContentType is the equivalent of the server variable CONTENT_TYPE.
When you use Internet Explorer 3.0 to send a POST request, lpszContentType is typically set to application/x-www-form-urlencoded.
Normally, the client browser sets the content type to ASCII. But it could be changed at the client side if you had to send binary data, for example.
To extract a server variable, you use the GetServerVariable, which is one of the functions in the EXTENSION_CONTROL_BLOCK. GetServerVariable is the equivalent to the getenv CGI function.
It is worth noting that the EXTENSION_CONTROL_BLOCK includes some server variables, as shown in Table 8.1.
Table 8.1 Compatible Entries
Control block | Server variable |
lpszMethod | REQUEST_METHOD |
lpszQueryString | QUERY_STRING |
lpszPathInfo | PATH_INFO |
lpszPathTranslated | PATH_TRANSLATED |
cbTotalBytes | CONTENT_LENGTH |
It is far more efficient to look at these variables using the EXTENSION_CONTROL_BLOCK because it does not include any function calls. To use GetServerVariable(), you pass it the current connection ID, the variable name you want, a buffer address to receive the contents of the variable, and the size of the buffer you are using.
You must use a pointer to a DWORD for the size of the buffer. This is because when it is completed, the DWORD is set to the amount of transferred data, including a null terminating byte.
Listing 8.6 VARIABLES.CPP-How to Get a Server Variable
DWORD cbdwbuff2;
CHAR ServerPort[20] = { 0 };
cbdwbuff2 = 10;
if( FALSE == pECB->GetServerVariable(pECB->ConnID, "SERVER_PORT", &ServerPort, &cbdwbuff2) )
{
DisplayError( GetLastError() );
}
If the call fails, GetServerVariable returns a value of FALSE. You can find the reason for the error by calling GetLastError.
Table 8.2 Possible Error Returns from GetServerVariable
Value | Meaning |
ERROR_INVALID_PARAMETER | Bad connection handle |
ERROR INVLAID_INDEX | Bad or incompatible variable identifier |
ERROR_INSUFFICIENT_BUFFER | Buffer too small, needed size returned in *lpdwSize |
ERROR_MORE_DATA | Buffer too small, only part of data returned. The total size of the data is not returned. |
ERROR_NO_DATA | The data request is not avalable. |
What Are Server Variables?
You can use server variables to supply information about the currently connected client and about the server itself. For example, you can learn the Internet protocol (IP) address of the client that has just called your extension or the type of browser used.
Listed below are the server variables, and a simple extension that shows you how POST and GET request methods affect server variables.
An overview of most server variables can be found in the CGI specification. This is maintained by the NCSA Software Development Group at http://hoohoo.ncsa.uiuc.edu/cgi/env.html.
![]()
Information from the Browser on Every Request
Server variables supply information about where and what the browser is. The variables with this information usually start with REMOTE_<type>-for example, REMOTE_ADDR or REMOTE_USER.
Other variables also supply client information such as HTTP_USER_AGENT, as explained later in this chapter.
Some Server-Specific Variables
Some variables provide information about the server that the extension is running on.
For example you might being writing a secure extension that has to run on only one server machine. For this, you could use the SERVER_NAME variable to check the machine name.
Or your extension might need a certain version of IIS to be running. For this, you would check the SERVER_SOFTWARE variable.
Useful for Configuring Custom Results
Different browsers have different capabilities. For example, Internet Explorer 1.x does not allow frames or tables, and Netscape 2.x does not allow scripts such as Visual Basic. So when you run an extension, you want to give that client the best output possible.
The extension can detect the type of browser connected by looking at the HTTP_USER_AGENT server variable, as shown in Table 8.3.
Table 8.3 Examples of Browser Type and Variable Setting
Browser | HTTP_USER_AGENT |
IE 2.0 | MSIE 2.0/Mozilla-Spoofer Mozilla/1.22 (compatible; MSIE 2.0; Windows 95) |
IE 3.0 | Mozilla/2.0 (compatible; MSIE 3.0; Windows NT) |
Netscape 3.0 | Mozilla/3.0 (WinNT; I) |
Netscape 2.02 | Mozilla/2.02 (Win16; I) |
Information collector for excite | ArchitextSpider |
Not everything that connects is a browser. There are a lot of spiders and robots out there. Spiders and robots are programs that automatically search the Web by following hypertext links that retrieve all documents referenced.
A list of robots and spiders is at http://info.webcrawler.com/mak/projects/robots/robots.html
![]()
Server Variables
Listing 8.7 is the code that shows the contents of the server variables, and how POST and GET can influence them. The listing includes all the server variables mentioned in this chapter. Use the Hypertext Markup Language (HTML) form in Listing 8.8 to try the extension with both request methods.
Listing 8.7 VARIABLES.CPP-Function That Displays Server Variables
//----------------------------------------------------------------------------
BOOL
DisplayVariable(char *lpszQuery, EXTENSION_CONTROL_BLOCK *pECB )
{
DWORD dwErr = 0;
DWORD cbdwbuff2;
DWORD dwLen = 0;
CHAR tmpbuf[4000] = { 0 };
CHAR ServerSoftware[200] = { 0 };
CHAR ServerProtocol[200] = { 0 };
CHAR RemoteAddress[200] = { 0 };
CHAR RemoteHost[200] = { 0 };
CHAR RemoteUser[200] = { 0 };
CHAR HttpAccept[200] = { 0 };
CHAR HttpUserAgent[356] = { 0 };
CHAR AuthType[200] = { 0 };
CHAR ContentLength[200] = { 0 };
CHAR ContentType[200] = { 0 };
CHAR GatewayInterface[200] = { 0 };
CHAR PathInfo[200] = { 0 };
CHAR PathTranslated[200] = { 0 };
CHAR QueryString[200] = { 0 };
CHAR RequestMethod[200] = { 0 };
CHAR ScriptName[200] = { 0 };
CHAR ServerName[200] = { 0 };
CHAR ServerPort[200] = { 0 };
CHAR AuthPass[200] = { 0 };
CHAR AllHttp[200] = { 0 };
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "AUTH_TYPE", &AuthType, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "CONTENT_LENGTH", &ContentLength, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "CONTENT_TYPE", &ContentType, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "GATEWAY_INTERFACE", &GatewayInterface, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "PATH_INFO", &PathInfo, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "PATH_TRANSLATED", &PathTranslated, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "QUERY_STRING", &QueryString, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "REQUEST_METHOD", &RequestMethod, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "SCRIPT_NAME", &ScriptName, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "SERVER_NAME", &ServerName, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "SERVER_PORT", &ServerPort, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "AUTH_PASS", &AuthPass, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "ALL_HTTP", &AllHttp, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "SERVER_PROTOCOL", &ServerProtocol, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "SERVER_SOFTWARE", &ServerSoftware, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "REMOTE_ADDR", &RemoteAddress, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "REMOTE_HOST", &RemoteHost, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "REMOTE_USER", &RemoteUser, &cbdwbuff2);
cbdwbuff2 = 200;
pECB->GetServerVariable(pECB->ConnID, "HTTP_ACCEPT", &HttpAccept, &cbdwbuff2);
cbdwbuff2 = 356;
pECB->GetServerVariable(pECB->ConnID, "HTTP_USER_AGENT", &HttpUserAgent, &cbdwbuff2);
//---------------------------------------------------
wsprintf(tmpbuf,"<p>\n"
"Auth Type = %s<br>\n"
"Content Length = %s<br>\n"
"Content Type = %s<br>\n"
"Gateway Interface = %s<br>\n"
"Path Info = %s<br>\n"
"Path Translated = %s<br>\n"
"Query String = %s<br>\n"
"Remote Address = %s<br>\n"
"Remote Host = %s<br>\n"
"Remote User = %s<br>\n"
"Request Method = %s<br>\n"
"Script Name = %s<br>\n"
"Server Name = %s<br>\n"
"Server Port = %s<br>\n"
"Server Protocol = %s<br>\n"
"Server Software = %s<br>\n"
"Auth Pass = %s<br>\n"
"All Http = %s<br>\n"
"HTTP ACCEPT = %s<br>\n"
"HTTP User Agent = %s<br>\n"
"</p>\n",
AuthType,
ContentLength,
ContentType,
GatewayInterface,
PathInfo,
PathTranslated,
QueryString,
RemoteAddress,
RemoteHost,
RemoteUser,
RequestMethod,
ScriptName,
ServerName,
ServerPort,
ServerProtocol,
ServerSoftware,
AuthPass,
AllHttp,
HttpAccept,
HttpUserAgent);
dwLen = lstrlen( tmpbuf );
pECB->WriteClient(pECB->ConnID, tmpbuf, &dwLen, dwLen );
return TRUE;
}
Listing 8.8 VARIABLES.HTM-HTML Code for Calling the variables.dll Extension
<html>
<head>
<title>Server Variables Display</title>
</head>
<body>
<h2>Get:</h2>
<form action="/scripts/variables.dll" method=get>
<INPUT NAME="param1" VALUE="" >
<input type="submit" value="Submit get Entry">
<input type="reset" value="Reset Form">
</form>
<hr>
<h2>Post:</h2>
<form action="/scripts/variables.dll" method=post>
<INPUT NAME="param1" VALUE="" >
<input type="submit" value="Submit post Entry">
<input type="reset" value="Reset Form">
</form>
</body>
</html>
Figure 8.2 is an example of running the form in Listing 8.8 with the GET request method.
Fig. 8.2Results of calling the display variables extension using the GET request method.
Following is a list of the server variables.
AUTH_TYPE. If the user name has been authenticated, this variable shows the authentication method used to validate the user. There is no set list because methods can be added to the base methods. The base methods are shown in Table 8.4.
Table 8.4 The Three Methods Supplied with IIS
Method | Variable contents |
Allow Anonymous | the variable is empty |
Basic clear text | basic |
Windows NT challenge/response | NTLM |
CONTENT_LENGTH. The total number of bytes received from the client when the request method is POST.
CONTENT_TYPE. Used to tell the extension the type of data sent by the client when the request method is POST.
For example, if the variable example in Figure 8.2 was called using the POST method with foo=bar as the passed data, we would get the following from this variable:
Content Type = application/x-www-form-urlencoded
and the CONTENT_LENGTH variable would be :
Content Length = 22
GATEWAY_INTERFACE. The revision of the CGI specification this server complies with. For IIS, this is CGI/1.1. The format is CGI/revision.
PATH_INFO. A variable with extra path information
as given by the client. This is the trailing part of the URL after
the extension name but before the query string. If you use the
variables extension again, call it like this:
http://staff/scripts/variables.dll/martin/?foo=var.
Figure 8.3 shows the output from the extension.
Notice that /martin/ is included between the end of the extension name and the query string.
![]()
Results of the PATH_INFO example.
PATH_TRANSLATED. This is the translated version of PATH_INFO,
which is the physical path mapping.
QUERY_STRING.The information following the "?" in the
URL that called the script. This is the request information and
it should not be decoded. This variable should always be set when
there is request information, regardless of command line decoding.
REMOTE_ADDR. The IP address of the remote client
making the request.
REMOTE_HOST. Normally set to the DNS host name of
the connected client if the server can do DNS lookups. IIS does
not do DNS lookup, so it is just set equal to REMOTE_ADDR.
REMOTE_USER. The user name supplied by the connected
client that IIS has authenticated.
UNMAPPED_REMOTE_USER. A copy of the user name before
any IIS filter has changed it-for example, by mapping the user
name to an NT user account name.
REQUEST_METHOD. The method used by the connected
client to make a request-for example, GET or POST.
SCRIPT_NAME. The name of the extension being executed.
SERVER_NAME. The server's host name, DNS alias,
or IP address.
SERVER_PORT. The port number to which the request
was sent. This is normally 80.
SERVER_PORT_SECURE. A Boolean value to indicate
if the request is on the secure port. If the result is 1, the
request is on the secure port. If the result is 0, it is not.
SERVER_PROTOCOL. The name and revision of the information
retrieval protocol this request came from. For example, the server
protocol from Figure 8.3 is HTTP/1.0, where the format is protocol/revision.
SERVER_SOFTWARE. The name and version of the IIS
that is running. The format of the variable is name/version. For
example, for IIS version 2.0, which comes with NT 4.0, the name
and version would be Microsoft-IIS/2.0.
AUTH_PASS. The password supplied by the user. This
is only supplied to a CGI script if the first character of its
name is "$." To guard against scripts "stealing"
user passwords, install $ scripts only from trusted sources.
Although AUTH_PASS is not used by IIS, it is possible to read the variable if AUTH_TYPE is BASIC.
You would have to query the HTTP_AUTHORIZATION header and then decode it.![]()
ALL_HTTP. All HTTP headers that were not already
parsed into one of the above variables. These variables are of
the form HTTP_<header field name>. Following is an example
of output from the variable:
HTTP_ACCEPT:image/gif, image/x-xbitmap, image/jpeg, image/pjpeg,
*/*
The two ways of writing to the client from the extension are using WriteClient and ServerSupportFunction.
ServerSupportFunction has limitations in that it can only send ASCII data to the client. It is used for sending completion status information and other header details to the client, as shown in Listing 8.9. An example is the content type of the data you are going to send to the client. Listing 8.9 VARIABLES.CPP-Partial Listing from HttpExtensionProcIf the request method was GET, all the data from the client can be read from the lpszQueryString buffer or from the QUERY_STRING server variable(as shown earlier in this chapter).
If the request method is POST, part or all of the data is in lpbData. If all the data is not in lpbData, you have to use ReadClient to read it. Listing 8.11 VARIABLES.CPP-Reading Data Sent by the ClientIf you do anything special when you start your extension that needs to be cleaned up when the extension is unloaded, this function is for you. But it is only available in Microsoft's IIS version 3.0 or later.
The function is called just before the server unloads the extension. A bit-field parameter is passed to the function by the server, indicating whether the extension has a choice about unloading.
Value | Meaning |
HSE_TERM_ADVISORY_UNLOAD | The server wants to unload the extension. The extension can return TRUE if that is OK, or FALSE if the extension does not want to unload. |
HSE_TERM_MUST_UNLOAD | The server wants to unload the extension and does not the extension a choice. At this point, the extension must clean up and be unloaded. |
You may want to redirect a connected client to another page on your server or even to another server all together. This section introduces you to the idea of redirecting a client to another URL without the user doing the connect.
Why Redirection? There are any number of reasons why you would want to redirect automatically from an extension. You might want a menu selection for your Web pages. Or you might want special pages for different kinds of browsers. Listing 8.12 is for a simple Web page menu system using a list box. You select the target you want and that information is passed to an extension called redir. Listing 8.12 REDIR.HTM-HTML Page with a Page Redirect MenuThis chapter introduces you to the capabilities you can use to write ISAPI extensions, including:
The following chapters give you more information on how to expand the capabilities of your extensions.