Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

ISAPI, IIS and Apache: I must be in Hell!


:P
On this page:

I spent a bit of time today working on cleaning up a number of issues with West Wind Web Connection’s integration with Apache. West Wind Web Connection provides an ISAPI extension that acts as the Web Server front end for the West Wind Web Connection Visual FoxPro framework. The DLL basically forwards request data to the application server and shuffles data back to the server from the VFP based application server/application handing the actual processing of Web Requests.

 

The tool is designed and primarily tested on the Windows Platform with IIS and it works very well with the Microsoft ISAPI implementation. Over the years every once in a while I’ve tested West Wind Web Connection with Apache and its ISAPI extension support, but the support had been pretty flaky at best. Couple that with a few slightly non-standard things that West Wind Web Connection’s ISAPI connector performs and you pretty much had an incompatible environment.

 

Part of the problem is that Apache does a number of things differently than IIS when handling requests. My biggest headache this weekend came from the fact that West Wind Web Connection sends headers as part of a full response that gets sent back from the application server. Basically the the Fox application framework handles building the entire HTTP output stream and sending it back for the ISAPI DLL to feed back through the Web server to the client.

 

A typical response from the App Server looks like this:

 

HTTP/1.1 200 OK

Content-type: text/html; charset=utf-8

Content-Length: 3042

 

<html>

<head>

<title>Unicode Foreign Language Text Demo</title>

<link rel="stylesheet" type="text/css" href="../westwind.css">

</head>

<body>

<h1>Unicode Foreign Language Text Demo</h1>

 

In the original code this text would then simply be written a chunk at a time (in 8-32k blocks depending on size)  to the server with ecb->WriteClient(). The original code never did a call to ecb->ServerSupportFunction() and that works fine in IIS.

 

Apache unfortunately does not like that and it bombs if no call to ServerSupportFunction() is made before sending additional content. In addition Apache is really finicky about how it accepts the headers and it took a fair amount of trial and error for me to figure out exactly what works.

 

Here’s what MSDN says about ServerSupportFunction() and the HSE_REQ_SEND_RESPONSE_HEADER constant:

This support function allows you to request that IIS send a complete HTTP response header to the client browser, including the HTTP status, server version, message time, and MIME version. Your extension can also, optionally, append other header information to the end of IIS-generated header, such as Content-Type or Content-Length.

 Notice that it says complete header, including HTTP status, server version etc.

 

This seems to suggest that I should just be able to split the header and body and then feed the header to ServerSupportFunction. It does work in IIS and it picks up the protocol and status code from the header returned.

 

Apache however chokes on a complete header. The status code must be sent separately. So you’d send headers as:

 

Content-Type: text/html

Content-Length: 4401

 

Make sure you add the 2 trailing CRLFs! For Apache you have to make sure that when you pass a block of headers that you terminate the headers with at least 1 carriage return linefeed pair, preferably two. Apache expects two or it doesn’t work correctly. If you forget this the headers will include your content body which is likely to crash your server if the content is sizable (believe me it looks very weird when this happens – the content will be in the middle of your headers followed by more headers AFTER your content – very bizarre).

 

A couple of things change once you send headers with ServerSupportFunction() as opposed to raw headers. When I sent raw headers in my output from the App Server they were treated as raw headers and went straight to the client. By using ServerSupportFunction() and HSE_REQ_SEND_RESPONSE_HEADER IIS will inject its own default headers like server name and default connection settings. Apache does this as well and this is probably a good thing. The other thing is that if your headers are screwy you get an invalid response from the server.

 

I can tell you that figuring this out was not easy because I couldn’t find any good information on what Apache does different. It worked in IIS, and Apached only failed with an Invalid Parameter API error. But man am I glad for Fiddler which helped tremendously in seeing what headers were being generated in both IIS and Apache.

 

All of this led to rewriting the output routines of which there are a few in the application. West Wind Web Connection now does the following:

 

  • Checks to see if there’s a header in the App Server generated output (checking for HTTP and then for the dual CRLFs)
  • If no header it adds a default header of text/html content type
  • If there’s a header it splits the content and header
  • Strips the HTTP status line for the headers
  • parses out just the status from the HTTP status header (HTTP/1.1 200 OK -> 200 OK)

 

The whole thing then ends up looking like this for the string based version (there’s also file based version which reads the output from a file for file based messaging):

 

void wwOle::SendOutput(EXTENSION_CONTROL_BLOCK *pEcb,

                        char *pszResults,

                        DWORD buflen,

                        char *szErrorMessage)   {

 

#define BUFFER_SIZE 16384

 

int lnSizeLeft;

DWORD lnSize, lnWritten,lnResult;

CHAR *pszWork;

 

if (!pEcb)

   return;

 

__try {

      /// *** If we passed a Null instead of a pointer, don't display output

      if (strlen(szErrorMessage)>0) {

               StandardPage(TEXT("An error occurred  during COM object invokation."),szErrorMessage,pEcb,"OleError");

               return;

      }

 

    /// No Output from server

      if (buflen < 2) {

        StandardPage(TEXT("Output Error"),

                     TEXT("The COM object returned no output."),pEcb,"NoOutput");

        return;

      }

 

      /// Send the Buffer to the Web Server

      /// Note we're chunking the output in 16k chunks to work around some secure

      /// page problems.

    lnSizeLeft = buflen;  // Strip the safety NULL

    pszWork = pszResults;

 

 

      /// If no header was supplied add a default one!

      /// Check for "HTTP" in first 4 letters of output

      if (_strnicmp(pszResults,"HTTP",4) != 0)

      {

            // *** No Http header - add a default header

            WriteHttpHeader(pEcb,"Content-type: text/html\r\n\r\n",NULL);

      }

      else

      {

                  // *** find end of header

                  char *Header = strstr((const char *) pszWork,"\r\n\r\n");

 

                  // *** if we have no header end just send it as part of request

                  // *** Otherwise split it out with

                  if (Header) {

                        Header = Header + 4;  // skip over CRLFs

                       

                        char StartChar = *Header;

                        *Header = 0;  // null header string

 

                        // *** strip out the HTTP Status string

                        char *lpHeader = strstr((const char *) pszWork,"\r\n");

                        *lpHeader=0;

                        lpHeader = lpHeader + 2;

 

                        // *** Get the Status String separately

                        char *lpStatus = strstr((const char *) pszWork," ");

                        lpStatus++;

 

                        lnSize = strlen((const char *) lpHeader);

                        BOOL Result = pEcb->ServerSupportFunction (

pEcb->ConnID,

                                                HSE_REQ_SEND_RESPONSE_HEADER,

                                                lpStatus,

                                                &lnSize,

                                                (LPDWORD) lpHeader);

 

                        *Header = StartChar;  // swap null for original char

                 

                        // *** Reposition our Read Pointer

//     and update the remaining byte count!

                        pszWork = Header; 

                        lnSizeLeft = lnSizeLeft - ( (int) Header - (int) pszResults );

                  } 

      } // Header Splitting

 

 

      // *** Loop through the content of the string

    while (lnSizeLeft > 0) {

         lnSize=(lnSizeLeft > BUFFER_SIZE) ? BUFFER_SIZE : lnSizeLeft;                                 

         lnWritten=lnSize;

         lnResult=pEcb->WriteClient(pEcb->ConnID,(void *)pszWork,&lnWritten,0);

 

         pszWork = pszWork + lnWritten;   // jump the buffer pointer

         lnSizeLeft = lnSizeLeft - lnWritten;

    } // while

  

}

__except(EXCEPTION_EXECUTE_HANDLER) {

      strcpy(szErrorMessage,"SendOutput: Exception occurred in Output generation and cleanup code (ExitCall).");

      LogError(szErrorMessage,pEcb);

}

 

return;

}

 

For those not very familiar with ISAPI, WriteClient() writes a chunk of data out to the client, but it doesn’t actually guarantee that everything you write gets written by WriteClient(). Hence you have to check and step through the entire string.

 

Now I have another problem that I can’t figure out this minute: If I return an Authentication header like this for Basic Authentication:

 

Content-Type: text/html

WWW-Authenticate: Basic realm="localhost"

 

and return a status code of 401 Not Authorized, IIS will now generate its own Authentication header. In fact it will generate:

 

HTTP/1.1 401 Not Authorized

Connection: close

Date: Mon, 20 Dec 2004 11:12:28 GMT

Server: Microsoft-IIS/6.0

WWW-Authenticate: Negotiate

WWW-Authenticate: NTLM

WWW-Authenticate: Basic realm="localhost"

WWW-Authenticate: basic realm="localhost"

Content-Type: text/html

Proxy-Support: Session-Based-Authentication

 

This would be cool if it did the right thing here, but it doesn’t. This is my application asking for basic authentication but because of the order of things generated IIS checks NTLM permissions on the file (the DLL or script) first, which is not at all what I want here. This means I would always authenticate even if my app might deny me rights based on Win

 

This really bites. So now it looks like I’ll have to run separate header routines for IIS and Apache. Using straight headers for IIS and using ServerSupportFunction() for Apache. Maybe somebody has some input here.

 

Incidentally I must be needing sleep <g>. I just tried to put go back to sending the headers with the content and now Fiddler is just eating my body and failing. This was not the case with my old code. Here’s what I’m doing:

 

char *T = new char[ lstrlen(Header) + 80 ];

 

*T = 0;

strcat(T,"HTTP/1.1 ");

if (StatusString)

      strcat(T,StatusString);

else

      strcat(T,"200 OK");

strcat(T,"\r\n");

strcat(T,Header);

DWORD buflen = lstrlen(T);

DWORD Result = pECB->WriteClient(pECB->ConnID,(void *) T,&buflen,0);

if (Result == FALSE)

      return false;

 

delete(T);

 

It looks like everything gets written properly here, but somehow Fiddler stops reading because there’s no content-length provided and it sees the end at the first chunk coming from IIS. Previously the data came in with the body and header in a single packet. I wouldn’t think that this should make a difference since ISAPI is sending this stuff out quickly, but apparently there’s some sort of chunking happening and Fiddler just stops reading.

 

<shrug>

 

Enough for today. Maybe somebody has some input on how to get things Authentication to work properly with ServerSupportFunction. It looks like HSE_REQ_SEND_RESPONSE_HEADER_EX would work, but APACHE apparently doesn’t support that so I’m back to square one.

 

 


The Voices of Reason


 

Rick Strahl
December 20, 2004

# re: ISAPI, IIS and Apache: I must be in Hell!

It looks like the inability to write out content with headers embedded is a known (but not confirmed or addressed) issue:

http://issues.apache.org/bugzilla/show_bug.cgi?id=30033

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2025