In the Chapter 9 discussion of ASP, our examination of how ASP was implemented on IIS led to the discovery that the ASP.dll responsible for implementing ASP is in fact an ISAPI DLL, exporting the functions required of any ISAPI extension. This discovery gives developers a powerful tool if they should require special scripting support not possible with the standard ASP scripting. Why might this be useful?
Of these three reasons, the first seems to be the most prevalent among people who have approached me on the subject. On several occasions, Perl and Python advocates have approached me after one of my talks on ISAPI. While implementing a scripting language is not for the squeamish and is fraught with manifold difficulties, it is not an overwhelming task.
For demonstration purposes, I created a simple ISAPI extension using MFC. The complete IspMFC_NG project is on the companion CD, though it is there merely as an example of what ended up not working. I set up the extension to translate any file with an extension of .isp (Internet scripting program). One of the first things I needed to do was to get the full path of the current script being executed so that my program could read and translate, as needed, the script I requested . This led to a default function similar to the following function that as yet contained no actual translation code:
voidCIspExtension::Default(CHttpServerContext*pCtxt) { charpath[255]; charscript[255]; DWORDcBytes=255; StartContent(pCtxt); WriteTitle(pCtxt); *pCtxt<<_T("Pathinformation<BR>"); pCtxt->GetServerVariable("PATH_TRANSLATED",path,&cBytes); *pCtxt<<path<<"<BR>\r\n"; pCtxt->GetServerVariable("SCRIPT_NAME",script,&cBytes); *pCtxt<<script<<"<BR>\r\n"; strcat(path,"\"); strcat(path,&script[1]); *pCtxt<<path<<"<BR>\r\n"; EndContent(pCtxt); } |
When called directly as a normal ISAPI extension ( http://server/path/isp.dll? ), the extension worked as expected, giving me the proper path for the "script" being executed, in my example, c:\inetpub\ wwwroot \isp.dll. Next I used the IIS configuration screen shown in Figure 10-1 and associated the .isp extension with this ISP.dll. I created a simple file with an .isp extension and used a URL that specified the name of the script file ( http://server/path/test.isp ), and the path came back as expected, c:\inetpub\wwwroot\test.isp.
Finally I tried adding a query string to the end of the URL. Here is where the intelligence of the MFC ISAPI classes got in my way. I received a status 400 in the browser when the URL with the query string was called. This made sense. The framework could not parse my query and find an appropriate function. Of course, it was possible to override the CallFunction method, but this seemed like overkill for the fairly simple requirement to funnel all requests to a single function to interpret a script.
At this point I was at a crossroads , one familiar to folks who use frameworks like MFC. I had two choices. First I could dig farther into the framework and override the functionality at the core of the CHttpServer class. This was in some ways an attractive solution because the ISAPI classes are interesting to investigate and the convenience of using the classes was clear. Comparing the code for the initial simple raw ISAPI example to the MFC examples reminded me of this convenience.
On the other hand, what was my goal? Was I looking to become an expert at the MFC ISAPI classes, or was I looking to get a scripting DLL done? I knew how much work I had to do to get the raw ISAPI scripting DLL done. Was I comfortable enough with the MFC classes to be sure I was not buying into a lot more work than I wanted? Would my customizations to the CallFunction method come back to haunt me? Were the MFC parsing features overkill when all scripting requests would be funneled into a single function in any event?
All these concerns led me to create a raw ISAPI scripting DLL. Given that the basic framework was in place already, I took the code from the original raw ISAPI extension and converted it for use as a scripting DLL. The scripting being done in this simple example was as follows . Wherever I saw the string "!date", I replaced it with the date. I replaced "!time" with the time, and I replaced "!name" with "Doug Reilly". The resulting source is presented in Listing 10-7.
Listing 10-7
ISPRaw.cpp //ISPRaw.cpp:DefinestheentrypointfortheDLLapplication. // #include"stdafx.h" BOOLAPIENTRYDllMain(HANDLEhModule, DWORDul_reason_for_call, LPVOIDlpReserved) { returnTRUE; } BOOLWINAPIGetExtensionVersion(HSE_VERSION_INFO*pVer) { pVer->dwExtensionVersion=1; strcpy(pVer->lpszExtensionDesc, "ISAPI1TestforInsideWindowsServerDevelopment"); return(TRUE); } DWORDWINAPIHttpExtensionProc(EXTENSION_CONTROL_BLOCK*lpECB) { DWORDcBuf; DWORDcBytes; charszMessage[1024]; charpath[255]; charquery[255]; FILE*in; HSE_SEND_HEADER_EX_INFOHeaderExInfo; //Prepareaheader.... HeaderExInfo.pszStatus="200OK"; HeaderExInfo.cchStatus=strlen(HeaderExInfo.pszStatus); HeaderExInfo.pszHeader="Content-type:text/html\r\n\r\n"; HeaderExInfo.cchHeader=strlen(HeaderExInfo.pszHeader); HeaderExInfo.fKeepConn=FALSE; //SendheadersusingIIS-providedcallback. //ThesefunctionsareusedquiteabitinISAPIprogramming. lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &HeaderExInfo, NULL, NULL); //Getthepaththatwaspassedinforthisscript,translated //toaformatappropriateforfilereading.... lpECB->GetServerVariable(lpECB->ConnID, "PATH_TRANSLATED",path,&cBytes); //Intherealworld,wewouldmakethequerystringavailable //tothescriptaswell. cBytes=255; lpECB->GetServerVariable(lpECB->ConnID, "QUERY_STRING",query,&cBytes); in=fopen(path,"rt"); if(in!=0) { intch; charszTemp[100]; szMessage[0]='// ISPRaw.cpp : Defines the entry point for the DLL application. // #include "stdafx.h" BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE; } BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = 1; strcpy (pVer->lpszExtensionDesc, "ISAPI1 Test for Inside Windows Server Development"); return(TRUE); } DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB) { DWORD cBuf; DWORD cBytes; char szMessage[1024]; char path[255]; char query[255]; FILE *in; HSE_SEND_HEADER_EX_INFO HeaderExInfo; // Prepare a header.... HeaderExInfo.pszStatus = "200 OK"; HeaderExInfo.cchStatus = strlen(HeaderExInfo.pszStatus); HeaderExInfo.pszHeader = "Content-type: text/html\r\n\r\n"; HeaderExInfo.cchHeader = strlen(HeaderExInfo.pszHeader); HeaderExInfo.fKeepConn = FALSE; // Send headers using IIS-provided callback. // These functions are used quite a bit in ISAPI programming. lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &HeaderExInfo, NULL, NULL); // Get the path that was passed in for this script, translated // to a format appropriate for file reading.... lpECB->GetServerVariable(lpECB->ConnID, "PATH_TRANSLATED", path,&cBytes); // In the real world, we would make the query string available // to the script as well. cBytes = 255; lpECB->GetServerVariable(lpECB->ConnID, "QUERY_STRING", query,&cBytes); in = fopen(path, "rt"); if (in != 0) { int ch; char szTemp[100]; szMessage[0] = '\0'; // Read through the script... while ((ch = fgetc(in)) != EOF) { // ...until we see our special signal character. if (ch != '!') { // Just pass along the text. szTemp[0] = ch; szTemp[1] = '\0'; strcat(szMessage, szTemp); } else { // Get the 4 characters after the '!'. szTemp[0] = fgetc(in); szTemp[1] = fgetc(in); szTemp[2] = fgetc(in); szTemp[3] = fgetc (in); szTemp[4] = '\0'; // Copy in the time in place of '!time'. if (!(strnicmp(szTemp, "time",4))) { char szTime[255]; _strtime(szTime); strcat(szMessage, szTime); } // Copy in the date in place of '!date'. else if (!(strnicmp(szTemp, "date", 4))) { char szDate[255]; _strdate(szDate); strcat(szMessage, szDate); } // Copy in my name in place of '!name'. else if (!(strnicmp(szTemp, "name", 4))) { strcat(szMessage, "Douglas Reilly"); } else { strcat(szMessage, "!"); strcat(szMessage, szTemp); } } } } // Set the length of the buffer being written. cBuf = strlen(szMessage); // Use another of the functions provided by IIS to write content // to the browser. lpECB->WriteClient(lpECB->ConnID, szMessage, &cBuf, HSE_IO_SYNC); return HSE_STATUS_SUCCESS; }'; //Readthroughthescript... while((ch=fgetc(in))!=EOF) { //...untilweseeourspecialsignalcharacter. if(ch!='!') { //Justpassalongthetext. szTemp[0]=ch; szTemp[1]='// ISPRaw.cpp : Defines the entry point for the DLL application. // #include "stdafx.h" BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE; } BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = 1; strcpy (pVer->lpszExtensionDesc, "ISAPI1 Test for Inside Windows Server Development"); return(TRUE); } DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB) { DWORD cBuf; DWORD cBytes; char szMessage[1024]; char path[255]; char query[255]; FILE *in; HSE_SEND_HEADER_EX_INFO HeaderExInfo; // Prepare a header.... HeaderExInfo.pszStatus = "200 OK"; HeaderExInfo.cchStatus = strlen(HeaderExInfo.pszStatus); HeaderExInfo.pszHeader = "Content-type: text/html\r\n\r\n"; HeaderExInfo.cchHeader = strlen(HeaderExInfo.pszHeader); HeaderExInfo.fKeepConn = FALSE; // Send headers using IIS-provided callback. // These functions are used quite a bit in ISAPI programming. lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &HeaderExInfo, NULL, NULL); // Get the path that was passed in for this script, translated // to a format appropriate for file reading.... lpECB->GetServerVariable(lpECB->ConnID, "PATH_TRANSLATED", path,&cBytes); // In the real world, we would make the query string available // to the script as well. cBytes = 255; lpECB->GetServerVariable(lpECB->ConnID, "QUERY_STRING", query,&cBytes); in = fopen(path, "rt"); if (in != 0) { int ch; char szTemp[100]; szMessage[0] = '\0'; // Read through the script... while ((ch = fgetc(in)) != EOF) { // ...until we see our special signal character. if (ch != '!') { // Just pass along the text. szTemp[0] = ch; szTemp[1] = '\0'; strcat(szMessage, szTemp); } else { // Get the 4 characters after the '!'. szTemp[0] = fgetc(in); szTemp[1] = fgetc(in); szTemp[2] = fgetc(in); szTemp[3] = fgetc (in); szTemp[4] = '\0'; // Copy in the time in place of '!time'. if (!(strnicmp(szTemp, "time",4))) { char szTime[255]; _strtime(szTime); strcat(szMessage, szTime); } // Copy in the date in place of '!date'. else if (!(strnicmp(szTemp, "date", 4))) { char szDate[255]; _strdate(szDate); strcat(szMessage, szDate); } // Copy in my name in place of '!name'. else if (!(strnicmp(szTemp, "name", 4))) { strcat(szMessage, "Douglas Reilly"); } else { strcat(szMessage, "!"); strcat(szMessage, szTemp); } } } } // Set the length of the buffer being written. cBuf = strlen(szMessage); // Use another of the functions provided by IIS to write content // to the browser. lpECB->WriteClient(lpECB->ConnID, szMessage, &cBuf, HSE_IO_SYNC); return HSE_STATUS_SUCCESS; }'; strcat(szMessage,szTemp); } else { //Getthe4charactersafterthe'!'. szTemp[0]=fgetc(in); szTemp[1]=fgetc(in); szTemp[2]=fgetc(in); szTemp[3]=fgetc(in); szTemp[4]='// ISPRaw.cpp : Defines the entry point for the DLL application. // #include "stdafx.h" BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE; } BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) { pVer->dwExtensionVersion = 1; strcpy (pVer->lpszExtensionDesc, "ISAPI1 Test for Inside Windows Server Development"); return(TRUE); } DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB) { DWORD cBuf; DWORD cBytes; char szMessage[1024]; char path[255]; char query[255]; FILE *in; HSE_SEND_HEADER_EX_INFO HeaderExInfo; // Prepare a header.... HeaderExInfo.pszStatus = "200 OK"; HeaderExInfo.cchStatus = strlen(HeaderExInfo.pszStatus); HeaderExInfo.pszHeader = "Content-type: text/html\r\n\r\n"; HeaderExInfo.cchHeader = strlen(HeaderExInfo.pszHeader); HeaderExInfo.fKeepConn = FALSE; // Send headers using IIS-provided callback. // These functions are used quite a bit in ISAPI programming. lpECB->ServerSupportFunction(lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &HeaderExInfo, NULL, NULL); // Get the path that was passed in for this script, translated // to a format appropriate for file reading.... lpECB->GetServerVariable(lpECB->ConnID, "PATH_TRANSLATED", path,&cBytes); // In the real world, we would make the query string available // to the script as well. cBytes = 255; lpECB->GetServerVariable(lpECB->ConnID, "QUERY_STRING", query,&cBytes); in = fopen(path, "rt"); if (in != 0) { int ch; char szTemp[100]; szMessage[0] = '\0'; // Read through the script... while ((ch = fgetc(in)) != EOF) { // ...until we see our special signal character. if (ch != '!') { // Just pass along the text. szTemp[0] = ch; szTemp[1] = '\0'; strcat(szMessage, szTemp); } else { // Get the 4 characters after the '!'. szTemp[0] = fgetc(in); szTemp[1] = fgetc(in); szTemp[2] = fgetc(in); szTemp[3] = fgetc (in); szTemp[4] = '\0'; // Copy in the time in place of '!time'. if (!(strnicmp(szTemp, "time",4))) { char szTime[255]; _strtime(szTime); strcat(szMessage, szTime); } // Copy in the date in place of '!date'. else if (!(strnicmp(szTemp, "date", 4))) { char szDate[255]; _strdate(szDate); strcat(szMessage, szDate); } // Copy in my name in place of '!name'. else if (!(strnicmp(szTemp, "name", 4))) { strcat(szMessage, "Douglas Reilly"); } else { strcat(szMessage, "!"); strcat(szMessage, szTemp); } } } } // Set the length of the buffer being written. cBuf = strlen(szMessage); // Use another of the functions provided by IIS to write content // to the browser. lpECB->WriteClient(lpECB->ConnID, szMessage, &cBuf, HSE_IO_SYNC); return HSE_STATUS_SUCCESS; }'; //Copyinthetimeinplaceof'!time'. if(!(strnicmp(szTemp,"time",4))) { charszTime[255]; _strtime(szTime); strcat(szMessage,szTime); } //Copyinthedateinplaceof'!date'. elseif(!(strnicmp(szTemp,"date",4))) { charszDate[255]; _strdate(szDate); strcat(szMessage,szDate); } //Copyinmynameinplaceof'!name'. elseif(!(strnicmp(szTemp,"name",4))) { strcat(szMessage,"DouglasReilly"); } else { strcat(szMessage,"!"); strcat(szMessage,szTemp); } } } } //Setthelengthofthebufferbeingwritten. cBuf=strlen(szMessage); //UseanotherofthefunctionsprovidedbyIIStowritecontent //tothebrowser. lpECB->WriteClient(lpECB->ConnID, szMessage,&cBuf,HSE_IO_SYNC); returnHSE_STATUS_SUCCESS; } |
The most significant part of the listing is within HttpExtensionProc . The header is sent as in the initial ISAPI example. Next the translated path is retrieved, using the GetServerVariable callback embedded in the EXTENSION_CONTROL_BLOCK pointer. The PATH_TRANSLATED server variable handles converting the virtual path into a path appropriate for use with the C standard file I/O functions. The fopen function is used to open the file, and characters are read in one at a time and appended to the szMessage character buffer that will be used to send information back to the browser.
While reading in characters from the script file, the character stream is monitored , and when the program sees an exclamation point (!), it checks the next four characters for the literal "time", "date", or "name". If the program finds a match, it replaces all five characters of the metatag with the time, date, or name. If the four characters after the ! character are not one of the strings to be substituted, the program appends the ! and the following four characters to the szMessage buffer.
Figure 10-12 shows the screen that results on a client browser when ISPRaw.dll processes the simple script shown in Listing 10-8. The result that ISPRaw.dll sends to the browser is standard HTML code.
Figure 10-12 The result of calling a simple ISP script processed by ISPRaw.dll.
Listing 10-8
<HTML> <Title>test</TITLE> <BODY> <Center><H1>Hi!</H1> <br><H2> Timeis!Time,dateis!date.<BR> Mynameis!Name.</H2> </CENTER> </BODY> </HTML> |