Implementing a Custom SOAP Server--The ISAPI Connection

By: Kenn Scribner for Visual C++ Developer


Download the code

Background: I have always enjoyed writine network protocol code (for some strange reason, but I also like assembly programming), and SOAP has been a treat to work with. While not what a traditional network protocol programmer would write (gotta be assembly for speed), SOAP is clearly a technology to be reckoned with. Learn XML and SOAP. Know XML and SOAP. With high-bandwidth pipes becoming commonplace (cable modems and DSL, for example), Internet-based services and applications will become much more common. Get ready.

Implementing a Custom SOAP Server--The ISAPI Connection

Since my recent article regarding SOAP, we've seen many implementations of the SOAP protocol arrive. DevelopMentor provided the first Perl and Java implementations, and Microsoft and Sun soon followed with implementations specific to their platforms. And although I'm not a corporate player, I was fortunate enough to join the fray with the COM interception/delegation architecture from the book Understanding SOAP (Sam's, ISBN 0672319225), some of the code from which I'll reuse in this article.

If you're one of those Microsoft technology consumers that is happy to incorporate the latest from Redmond into your work, I highly recommend you consider the Web Services Toolkit and even the .NET architecture as a whole. But that's another article.

This article is for the developer who not only wants to use the latest technology but also wants to know how it ticks...how it makes the magic happen. In this article you'll not be using an off-the-shelf SOAP processing architecture. Instead, you'll see how to create a rudimentary system from the ground up. If that sounds interesting to you then definitely read on. You might be surprised how easy it is to do, which is the beauty of all good magic.

A Basic SOAP Processing Architecture

Many people find the 'O' in "SOAP" confusing, because SOAP isn't about objects in the object-oriented sense. As a communications protocol it is far too basic for that. Instead, SOAP was designed to be a Remote Procedure Call (RPC) protocol, which is to say you use SOAP to convey a single remote method call. If the method call happens to come from a C++ or Java object, so much the better, but SOAP will be completely unaware of the object itself. From SOAP's viewpoint, that sort of higher-level information should be contained within the architecture that implements the SOAP protocol, if object-based computing is desired. This is very much like the DCE RPC protocol, which is the low-level protocol used by DCOM. DCOM manages the object-based aspects of the architecture, while DCE RPC handles the communications work. SOAP is analogous to DCE RPC, and you (or one of those corporate players I mentioned) must provide the DCOM aspects of the architecture.

The result of this is you needn't implement a grandiose system to implement a basic SOAP server. The only requirements you must meet are to accept and submit HTTP (or other protocol) packets, be able to interpret XML data, and to follow the SOAP specification when you create and interpret the SOAP data. There are a great number of Web servers available, so finding a good HTTP communications package isn't too difficult. Here, I'll use Microsoft's Internet Information Server, but I could have just as easily used Apache or WebSphere, or even rolled my own using socket programming techniques. Processing XML documents is a snap today because there are inexpensive, even free, XML parsers available. Once again I'll use the Microsoft DOM parser, but I could have chosen to use a SAX parser, Xerces, or simply interpreted the XML textual data myself. As for following the SOAP specification, I decided to wrap most of that functionality into a set of reusable C++ classes that together form a SOAP object model. I'll discuss these in the next section.

Therefore, the SOAP processing architecture I implemented for this article involves a SOAP server, written as an ISAPI extension, and a SOAP client, which I created using MFC (Figure 1). After all, this is for Visual C++ developers! In my opinion it would be more appropriate to roll client-side SOAP work into a COM object, but I wanted to keep the support code at a minimum. With COM, I would have to create an ATL-based COM server and plumb the SOAP calls into COM methods. While this isn't too difficult, I found exposing the SOAP mechanics as pure C++ methods reduced the code you would have to wade through to get to the good stuff (the SOAP code itself). If you share my opinion regarding client-side SOAP, feel free to play with the code you see here and roll it into your own COM object(s). Note that this doesn't mean I didn't use COM! Both the XML parser and the HTTP communications objects are COM-based.

The remainder of the article will be dedicated to describing the pieces that make up this architecture. I'll first describe the reusable SOAP C++ objects I created, then I'll briefly describe how ISAPI extensions work, and finally I'll cover in detail the code on both ends of the architecture

Reusable C++ SOAP Objects

When I first started working with SOAP, I either used the XML parser directly to create the SOAP payload, or I simply wrote the XML-tagged information to a text buffer and shipped that. Both methods worked, and each has their place, but it was clear to me that what I really needed was a set of SOAP objects that closely matched the SOAP processing model. That is, it would be nice to have a SOAP envelope object that automatically created a body object, and that would, on demand, create a header object. I could build into the envelope object the namespace intelligence SOAP requires that is not fully supported by the contemporary crop of XML parsers. I could even relinquish some of the SOAP details to a set of SOAP objects and allow them to format the SOAP document. That way, I don't need to keep re-writing the same code over and over again.

I further decided that I would want to have these objects be portable to any system, just in case I wanted to write SOAP code for other platforms. Given the wide availability of the Standard Template Library (STL) and easy access to C++ compilers on other systems, I elected to write a set of C++ based SOAP objects that use the STL as containers for various pieces of SOAP information. For example, child elements are kept in an STL vector. The data itself would be streamed to a C++ stream on demand, and that, in turn, could be converted into almost anything--text files, IStream data, or even STL strings if necessary. Using the built-in C++ support and the STL keeps things generic.

I won't describe the mechanics of the objects in detail here due to space constraints. I do describe them in some detail in Understanding SOAP, and if there is interest, I'll describe them in more detail in a future issue of Visual C++ Developer. Even so, there are some aspects of the objects I do need to cover because you'll see the objects in use when I discuss the ISAPI extension and MFC client.

Figure 2 provides you with the SOAP object hierarchy. Each object exposes public methods that allow you to work with some aspect of the SOAP specification. CSOAPElement, for example, is the base class for several derived objects, to ultimately include the envelope, the header, and the body. The higher-order objects derive much of their functionality from either their base class or other aggregated SOAP objects. Returning to CSOAPElement, as an example, you can see from Figure 2 that it contains an instance of CSOAPElementNamespace and CSOAPElementAttributes.

The main SOAP object is CSOAPHttpPacket, which contains both the HTTP headers and the XML payload for a given SOAP request/response. This is also the only object you need to explicitly create. All of the other objects will be created for you, upon request, and so long as you remember to destroy the instance of CSOAPHttpPacket all of the resources will be reclaimed (memory and so on).

With an instance of CSOAPHttpPacket comes an instance of CSOAPEnvelope and CSOAPBody. You gain access to these objects using CSOAPHttpPacket::GetEnvelope() and CSOAPEnvelope::GetBody() respectively. The SOAP header isn't automatically created for you, as it is optional. However, if you do want to work with the header, calling CSOAPEnvelope::GetHeader() will create the header object and return a pointer to you.

Given a pointer to the header or the body, you create independent elements using CSOAPScopingElement::InsertElement(). The concept of a scoping element went out with SOAP 1.0, but I felt it still had a place in my object model, so the envelope, header, and body all derive from the scoping element. Independent elements, in turn, can contain embedded elements, and you create them using CSOAPIndElement::InsertElement().

To some degree the objects dictate the form of the SOAP XML payload, but for the most part you are free to insert independent and embedded elements as required to support the given method invocation. The objects themselves really act more like a container than a rigid object model, and you're free to tailor the contents of the container to suit your needs.

Note that namespaces and attributes are also supported. The objects were written to support SOAP 1.1, so the SOAP 1.1 namespaces and standard attributes are easily inserted into your SOAP packet using CSOAPElementNamespace::EnableStdNamespace() and CSOAPElementAttributes::EnableStdAttribute(). If you have additional namespaces and attributes, you can insert those as well. When you insert custom namespaces, the namespace object will provide you with a cookie that you use to identify the namespace when using it with child elements.

When it's time to extract the SOAP XML-based contents from the objects, you call CSOAPHttpPacket::WriteToStream() and provide an instance of std::ostrstream that will contain the textual (XML) representation of the SOAP information. Given this stream you can put the information nearly anywhere--to a string or file, to an instance of an XML parser, or directly to the Internet if you elect to stream the HTTP header along with the XML data. I'll provide example code for many of the object model operations I described here when I discuss the server and client code.

The ISAPI Programming Model

While this article deals with SOAP and handling custom SOAP calls, I feel it's instructive to briefly introduce ISAPI as well. The reason for this is Microsoft Internet Information Server provides a ready-made receptacle for SOAP code, and the programming interface you use is ISAPI. (Note it is also true you could interpret SOAP method invocations using Active Server Pages, but I'll limit my discussion here to C++ based alternatives.)

If you reflect back to the early days of the Internet, perhaps 8 years ago, the initial crop of Web servers supported server-side customization only through the Common Gateway Interface, or CGI. The purpose of the CGI was for remote clients to be able to invoke server-side operations through a standard mechanism. Typically at this time in history, the specific CGI operations were implemented by Perl scripts and could become quite complicated.

When Microsoft joined the Internet boom and brought forth IIS, they provided a new mechanism for invoking server-side code. This mechanism was called Internet Server Application Programming Interface, or ISAPI. The ISAPI extension takes the form of a DLL that contains three well-known callback functions: GetExtensionVersion(); HttpExtensionProc(); and TerminateExtension(). The extension is designed to add processing capability to IIS, so it probably isn't too surprising to find IIS consumes the DLL and calls the callback functions at specific times when the extension's URL is accessed.

For our purposes, the truly interesting callback function is HttpExtensionProc(). GetExtensionVersion() and TerminateExtension() are used to register your ISAPI extension and terminate it respectively. These callbacks are management functions, and the code required to support them is relatively straightforward, albeit somewhat dependent upon the version of IIS you happen to be using. My code is designed for IIS 5.0 on Windows 2000, although it should work with IIS 4.0 on Windows NT 4.0 SP5.

HttpExtensionProc(), however, is called whenever IIS receives an HTTP packet destined for your extention's URL. This DLL method is where you insert code that processes the contents of the URL (a common way to pass argument data in traditional Internet programming paradigms) and/or the contents of the packet. Your implementation of this callback function will be provided a smorgasbord of information regarding the HTTP request. The information is rolled into a complex structure known as the EXTENSION_CONTROL_BLOCK. There is a lot of functionality built into the EXTENSION_CONTROL_BLOCK structure, and if you're interested in Internet-based programming and ISAPI in particular, I recommend locating a good tutorial and reference book on the subject. As it happens, for the purposes of the SOAP server I describe here relatively little of the functionality is required so I won't explain all of its features in this article.

What will be most important here is the HTTP method, stored in lpszMethod (GET, POST, and so on), and a pointer to the IIS function WriteClient(), which you'll use to send your SOAP response packet to the originating client. Access to WriteClient() is clearly important as that's how you send the result information back, but the HTTP method may not be so at first glance. The reason you're interested in the HTTP method is simply that SOAP calls are by definition POST operations (or possibly MPOST, but POST is preferred according to the specification). In some cases your SOAP server might patently fail an HTTP GET request. But in the particular case of the SOAP server I'll present I assume the HTTP GET came from a Web Browser. In this case I instead send a textual note to the client rather than fail the request or attempt to (incorrectly) process a bogus SOAP call.

Note: The code presented here is demonstrational. For low-throughput systems it should suffice, but for more profesional applications I'd do things differently (and have). This design is relatively flat, and I'd put very much more into COM objects. I'd also disable Nagling. I did things as I did here to emphasize the SOAP coding aspects rather than every possible design consideration when using ISAPI (although I refuse to use the buggy MFC ISAPI classes!).

The SOAP ISAPI Server

At some point the theory has to become concrete! So let's turn to the SOAP server. In this case, to simplify this SOAP server a bit, I decided to create a remote method with the following call signature:

String GetRemoteTime();

Essentially, you request a time hack from the remote server. The simplification is that the method has no input parameters, although it does return a single string value that represents the time (it will be encoded according to the XML specification, as you'll see). This makes the SOAP encoding a bit easier to deal with for an initial implementation. If you do want to extend the code to handle more complex methods, the technique I use to extract the returned string on the client can also be used to extract input parameters (on the server) or additional output values (on the client).

Given this method signature, the SOAP server will expect an incoming SOAP packet to look like the following:

<SOAP-ENV:Envelope
  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:m="www.yourhosthere.com"
  SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <SOAP-ENV:Body>
        <m:GetRemoteTime />
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

The server should extract the method name from the SOAP packet, along with any input parameter data, of which there is none in this case, and invoke the method locally and return the resulting value to the client. Since there are no method parameters associated with GetRemoteTime(), the server merely needs to extract the method name and make the invocation. I also dictated the format of the SOAPAction HTTP header, which should look like this when grouped within the packet's HTTP headers:

SOAPAction: "www.yourhosthere.com#GetRemoteTime"

The ISAPI extension code to decipher this SOAP packet is shown in Listing 1.

Listing 1. The SOAP Server's HttpExtensionProc() Method

// This method is triggered upon HTTP events within IIS
DWORD CVCDSoapExtension::HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpECB)
{
   // Make sure this was a POST request
   string strHTTPVerb(lpECB->lpszMethod);
   if ( strHTTPVerb != string("POST") ) {
      LPTSTR strMsg = _T("You found it...Kenn's SOAP demo ISAPI DLL!");
      ULONG bufLen = _tcslen(strMsg);
      lpECB->WriteClient(lpECB->ConnID, strMsg, &bufLen, HSE_IO_SYNC); 
      return HSE_STATUS_SUCCESS;
   } // if
   // Initialize COM runtime
   HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
   if ( FAILED(hr) ) return HSE_STATUS_ERROR;

 

   // Try to process the request
   DWORD dwRetCode = HSE_STATUS_SUCCESS;
   try {
      // Create the request DOM
      CComPtr<IXMLDOMDocument> pRequest;
      HRESULT hr = pRequest.CoCreateInstance(__uuidof(DOMDocument));
      if ( FAILED(hr) ) throw "Client.InitializationError#Unable to initialize the XML parser";

 

      // Load POSTed XML into DOM
      CComBSTR bstrTemp((char *)lpECB->lpbData);
      VARIANT_BOOL bSuccess = false;
      pRequest->loadXML(bstrTemp,&bSuccess);
      if ( FAILED(hr) ) throw "Client.RequestError#Unable to load SOAP data into the XML parser";
      if ( !bSuccess ) throw "Client.RequestError#Unable to load SOAP data into the XML parser";

 

      // Retrieve the "SOAPAction:" HTTP header field
      DWORD dwMethodHeaderLen = HEADER_BUF_SIZE;
      TCHAR szMethodHeader[HEADER_BUF_SIZE];
      BOOL bRC = lpECB->GetServerVariable(lpECB->ConnID, 
                                          "HTTP_SOAPACTION",
                                          szMethodHeader, 
                                          &dwMethodHeaderLen);
      if ( bRC == FALSE ) throw "Client.InvalidSOAPPacket#Missing a valid SOAPAction header";

 

      // Find the delimeter between URI and method name
      LPTSTR szMethodURI = _tcstok(szMethodHeader,"#");
      LPTSTR szMethod = _tcstok(NULL,"\0");

 

      // Create an Activator for this method
      CComBSTR bstrMethodURI(szMethodURI);
      CComBSTR bstrMethod(szMethod);
      CActivator act(bstrMethodURI,bstrMethod);

 

      // Invoke the object's method and retrieve a response
      CComPtr<IXMLDOMDocument> pResponse;
      act.Invoke(pRequest,&pResponse);

 

      // Retrieve the XML response
      if ( !pResponse.p ) throw "Client.ResponseError#Unable to retrieve XML/SOAP response from the XML parser";
      CComBSTR bstrXML;
      hr = pResponse->get_xml(&bstrXML);
      if ( FAILED(hr) ) throw "Client.ResponseError#Unable to retrieve XML/SOAP response from the XML parser";

 

      // Check the length of the result...if it is an empty
      // string, something horrid happened and we need to
      // return a (hardcoded) SOAP fault from here.
      if ( !bstrXML.Length() ) throw "Client.ResponseError#Unable to retrieve XML/SOAP response from the XML parser";

 

      // Send the response to the client
      ULONG bufLen = bstrXML.Length();
      USES_CONVERSION;
      lpECB->WriteClient(lpECB->ConnID, (void*)W2A(bstrXML), &bufLen, HSE_IO_SYNC);
   } // try
   catch(LPCTSTR strErr) {
      // The error string consists of two parts...the faultcode and
      // faultstring, which are separated by '#'. Separate those
      // parts for later formatting into a fault packet.
      dwRetCode = HSE_STATUS_ERROR;
      TCHAR strMsg[1024] = {0};
      _tcscpy(strMsg,strErr); // incoming string is const...
      LPTSTR szFaultCode = _tcstok(strMsg,"#");
      LPTSTR szFaultString = _tcstok(NULL,"\0");

 

      // Create a string with the formatted fault packet
      std::string strFormat = "<SOAP-ENV:Envelope"
      strFormat += "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' ";
      strFormat += "SOAP-ENV:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>";
      strFormat += "<SOAP-ENV:Body><SOAP-ENV:Fault><faultcode>>";
      strFormat += szFaultCode;
      strFormat += "</faultcode><faultstring>";
      strFormat += szFaultString;
      strFormat += "</faultstring></SOAP-ENV:Fault></SOAP-ENV:Body></SOAP-ENV:Envelope>";

 

      // Return the fault packet
      ULONG bufLen = strFormat.length();
      lpECB->WriteClient(lpECB->ConnID, (void*)strFormat.c_str(), &bufLen, HSE_IO_SYNC); 
} // catch
   catch(...) {
      // Unknown error, so return SOAP fault... We'll assume the
      // problem was with the POST and not the GET...
      dwRetCode = HSE_STATUS_ERROR;
      LPTSTR strMsg = "<SOAP-ENV:Envelope"
      "xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' "
      "SOAP-ENV:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>"
      "<SOAP-ENV:Body>"
      "<SOAP-ENV:Fault>"
      "<faultcode>Client.UnknownError</faultcode>"
      "<faultstring>Call your broker and dump all EnduraSoft stock immediately</faultstring>"
      "</SOAP-ENV:Fault>"
      "</SOAP-ENV:Body>"
      "</SOAP-ENV:Envelope>";
      ULONG bufLen = _tcslen(strMsg);
      lpECB->WriteClient(lpECB->ConnID, strMsg, &bufLen, HSE_IO_SYNC); 
   } // catch
   CoUninitialize();
   return dwRetCode;
}

The extension begins by testing for the HTTP method:

std::string strHTTPVerb(lpECB->lpszMethod);
if ( strHTTPVerb != std::string("POST") ) {
   LPTSTR strMsg = _T("You found it...Kenn's SOAP demo ISAPI DLL!");
   ULONG bufLen = _tcslen(strMsg);
   lpECB->WriteClient(lpECB->ConnID, strMsg, &bufLen, HSE_IO_SYNC); 
   return HSE_STATUS_SUCCESS;
} // if

If the incoming HTTP method was GET, the extension returns a textual message and quits. If the HTTP method was a POST, however, the SOAP processor continues by initializing the COM runtime and creating an instance of the Microsoft XML DOM parser. Now the truly interesting SOAP work begins.

We begin the SOAP parsing by first loading the XML data into the parser:

// Load POSTed XML into DOM
CComBSTR bstrTemp((char *)lpECB->lpbData);
VARIANT_BOOL bSuccess = false;
pRequest->loadXML(bstrTemp,&bSuccess);
if ( FAILED(hr) ) throw "Client.RequestError#Unable to load SOAP data into the XML parser";
if ( !bSuccess ) throw "Client.RequestError#Unable to load SOAP data into the XML parser";

Then we retrieve the SOAPAction header:

// Retrieve the "SOAPAction:" HTTP header field
DWORD dwMethodHeaderLen = HEADER_BUF_SIZE;
TCHAR szMethodHeader[HEADER_BUF_SIZE];
BOOL bRC = lpECB->GetServerVariable(lpECB->ConnID, 
"HTTP_SOAPACTION",
szMethodHeader, 
&dwMethodHeaderLen);
if ( bRC == FALSE ) throw "Client.InvalidSOAPPacket#Missing a valid SOAPAction header";

The reason for this is that eventually we'll want to check the method prescribed in the SOAPAction header against what we find in the XML payload. So after some string manipulation, we create an instance of an object I call the "activator". The activator handles the meat of the specific SOAP method request--in this manner I can keep the same basic ISAPI framework and insert a specific activator object for a given method call.

In this case, the activator object will handle the GetRemoteTime() method:

CActivator act(bstrMethodURI,bstrMethod);

We provide the activator with the method's URI and name, after which we invoke the method itself (I'll discuss the method code itself shortly):

// Invoke the object's method and retrieve a response
CComPtr<IXMLDOMDocument> pResponse;
act.Invoke(pRequest,&pResponse);

By one way or another the activator will return a SOAP packet. It may be a fault packet, or it may be a valid method return packet. In either case, we should be able to retrieve the packet from our local instance of the response XML parser:

// Retrieve the XML response
if ( !pResponse.p ) throw "Client.ResponseError#Unable to retrieve XML/SOAP response from the XML parser";
CComBSTR bstrXML;
hr = pResponse->get_xml(&bstrXML);
if ( FAILED(hr) ) throw "Client.ResponseError#Unable to retrieve XML/SOAP response from the XML parser";

After we do some basic checking, such as the length of the string as well as the HRESULT that came from retrieving the XML data, we send the response packet back to the client:

// Send the response to the client
ULONG bufLen = bstrXML.Length();
USES_CONVERSION;
lpECB->WriteClient(lpECB->ConnID, (void*)W2A(bstrXML), &bufLen, HSE_IO_SYNC);

From Listing 1 you can also see I do some basic error handling using try/catch blocks. I always try to return some SOAP packet to the client if at all possible, so if I know the error, I construct a SOAP fault packet. If something catastrophic happened, I return a hard-coded fault packet.

Note that in any case I'm returning a valid HTTP response. That isn't to say the SOAP response is or is not a fault packet. There is a debate between two factions in the SOAP community today regarding the return of SOAP fault packets. One camp claims that even if the SOAP processor returns a fault, the HTTP plumbing should return a valid HTTP response code ("200 OK"). This tells the client the error was with SOAP and not the communication of the SOAP data. The other camp claims that SOAP faults should always be encoded in an error HTTP response ("500 Server Error"). I prefer the first construct and have coded things accordingly. Over time this detail, as well as others, should be ironed out with the release of updated SOAP specifications. With any luck, my prediction will come true and I won't have to change my code!

In any case, let's now turn to the activator object and see what is involved with returning the server's time. The SOAP response packet, assuming we're not returning a fault, will look like the following:

<SOAP-ENV:Envelope
  xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:m="www.yourhosthere.com"
  SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <SOAP-ENV:Body>
        <m:GetRemoteTimeResponse>
            <return>
                2000-02-01T14:05:00
            </ return>
        </ m:GetRemoteTimeResponse>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

The difference between the request packet and the response packet is the method name has the string Response concatenated and the response element has a child element <return /> that contains the returned time string (which I'll discuss in a moment).

The GetRemoteTime() activator method that produces this SOAP packet is wrapped in CActivator::Invoke(), as you see in Listing 2.

Listing 2. The Activator's Invoke() Method

//
// Invoke the method call based on the incoming XML
// request and generate an XML response from the result.
//
void CActivator::Invoke(IXMLDOMDocument *pRequest,
                        IXMLDOMDocument **ppResponse)
{
   try {
      // Build outgoing XML (caller is responsible for
      // releasing this interface)
      HRESULT hr = CoCreateInstance(__uuidof(DOMDocument), 
                                    0, 
                                    CLSCTX_ALL, 
                                    __uuidof(IXMLDOMDocument), 
                                    reinterpret_cast<void **>(ppResponse));
      if ( FAILED(hr) ) throw hr;

 

      // Check the HTTP SOAPAction header to see if the name of
      // the method there reflects the method we find in the 
      // body.
      //
      // Check for <SOAP-ENV:Envelope>
      // <SOAP-ENV:Body>
      //
      // Construct search string "SOAP-ENV:Envelope/SOAP-ENV:Body"
      CComBSTR bstrSS(L"SOAP-ENV:Envelope/SOAP-ENV:Body");

 

      // Find the body
      CComPtr<IXMLDOMNode> spBodyNode;
      hr = pRequest->selectSingleNode(bstrSS,&spBodyNode);
      if ( FAILED(hr) ) throw hr;

 

      // The method must be the first child of the body
      CComPtr<IXMLDOMNode> spMethodNode;
      hr = spBodyNode->get_firstChild(&spMethodNode);
      if ( FAILED(hr) ) throw hr;

 

      // Ensure that the HTTP method name matches the payload
      // method name.
      CComBSTR bstrPayloadMethodName;
      hr = spMethodNode->get_baseName(&bstrPayloadMethodName);
      if ( wcscmp(bstrPayloadMethodName,m_bstrMethod) ) {
         // Wrong method, or at least the packet information
         // doesn't agree. The method name in the SOAPAction
         // header should match what's in the XML payload.
         throw E_NOTIMPL;
      } // if

 

      // Check for supported method name "GetRemoteTime"
      std::string strReturn;
      char szData[33] = {0};
      if ( !_wcsicmp(L"GetRemoteTime",m_bstrMethod) ) {
         // Obtain the server's system time
         SYSTEMTIME st;
         GetSystemTime(&st);

 

         // Format: 2000-02-01T14:05:00
         // Note the server will return its time as
         // UTC, so no bias will be required...
         //
         // Convert year first
         itoa(st.wYear,szData,10);
         strReturn = szData;
         strReturn += "-";

 

         // Convert month
         itoa(st.wMonth,szData,10);
         strReturn += szData;
         strReturn += "-";

 

         // Convert day
         itoa(st.wDay,szData,10);
         strReturn += szData;
         strReturn += "T";

 

         // Convert hour
         //
         // Note SYSTEMTIME is in UTC,
         // so no bias is required
         itoa(st.wHour,szData,10);
         strReturn += szData;
         strReturn += ":";

 

         // Convert minute
         itoa(st.wMinute,szData,10);
         strReturn += szData;
         strReturn += ":";

 

         // Convert second
         itoa(st.wSecond,szData,10);
         strReturn += szData;
      } // if
      else {
         // Fault...we don't support this method
         throw E_NOTIMPL;
      } // else

 

      // Create response packet
      CreateResponse((*ppResponse),strReturn);
   } // try
   catch(HRESULT hrErr) {
      // Create fault packet
      if ( (*ppResponse) != NULL ) {
         CreateFault((*ppResponse),hrErr);
      } // if
   } // catch
   catch(...) {
      // Create fault packet
      if ( (*ppResponse) != NULL ) {
         CreateFault((*ppResponse),E_UNEXPECTED);
      } // if
   } // catch
}

The activator begins by creating an instance of the XML parser, which it will use to return the SOAP data. Then, we check the method name from the SOAPAction HTTP header against what we find in the SOAP body:

// Check the HTTP SOAPAction header to see if the name of
// the method there reflects the method we find in the 
// body.
//
// Check for <SOAP-ENV:Envelope>
// <SOAP-ENV:Body>
//
// Construct search string "SOAP-ENV:Envelope/SOAP-ENV:Body"
CComBSTR bstrSS(L"SOAP-ENV:Envelope/SOAP-ENV:Body");

 

// Find the body
CComPtr<IXMLDOMNode> spBodyNode;
hr = pRequest->selectSingleNode(bstrSS,&spBodyNode);
if ( FAILED(hr) ) throw hr;

 

// The method must be the first child of the body
CComPtr<IXMLDOMNode> spMethodNode;
hr = spBodyNode->get_firstChild(&spMethodNode);
if ( FAILED(hr) ) throw hr;

 

// Ensure that the HTTP method name matches the payload
// method name.
CComBSTR bstrPayloadMethodName;
hr = spMethodNode->get_baseName(&bstrPayloadMethodName);
if ( wcscmp(bstrPayloadMethodName,m_bstrMethod) ) {
   // Wrong method, or at least the packet information
   // doesn't agree. The method name in the SOAPAction
   // header should match what's in the XML payload.
   throw E_NOTIMPL;
} // if

We do this using basic XML DOM methods. For example, we obtain a pointer to the body element and extract the first child node, which must be the method node. We decide if we should continue processing by extracting the name of the method node and comparing that to the method name we were given when the object was constructed (which came from the HTTP header),. If we do continue, we then check to make sure we're dealing with a remote time request:

// Check for supported method name "GetRemoteTime"
std::string strReturn;
char szData[33] = {0};
if ( !_wcsicmp(L"GetRemoteTime",m_bstrMethod) ) {
   (...process remote time code here...)
} // if
else {
   // Fault...we don't support this method
   throw E_NOTIMPL;
} // else

If we did retrieve the time, we stuff it into the response SOAP packet:

// Create response packet
CreateResponse((*ppResponse),strReturn);

The time processing itself is relatively unremarkable with the exception of the formatting of the response. Dates, time, and time durations, are to be encoded in SOAP according to the XML specification itself:

2000-02-01T14:05:00-5:00

The format is year-month-day for date information and hours:minutes:seconds for time. The date and time are separated by the literal T, and any bias trails the time. The bias, which is minus 5 hours in this case, is the difference between the time indicated (2:05 in the afternoon) and UTC (Universal Coordinated Time, formerly known as Greenwich Mean Time). If there is no bias, you must assume the time indicated is UTC time. In this case, the example time string you see here tells you it is February 1, 2000 at 2:05pm Eastern.

The SOAP activator object also has two helper functions I use to create SOAP packets. CreateResponse() builds a SOAP response packet while CreateFault() builds a fault packet for return to the client. I've shown the code for CreateResponse() in Listing 3. CreateFault() is similar even if it does create a very different SOAP packet.

Listing 3. The Activator's CreateResponse() Method

void CActivator::CreateResponse(IXMLDOMDocument *pResponse,
                                const std::string& strReturn)
{
   // Build a successful SOAP response packet
   CSOAPHttpPacket* pPacket = NULL;
   try {
      // Build SOAP HTTP packet (only used as a container
      // for the Envelope)
      SOAPCALLINFO ciInfo;
      ciInfo.strHost = "none";
      ciInfo.strPostHdr = "none";
      ciInfo.strMethod = "none"; // won't be used for return packet
      pPacket = new CSOAPHttpPacket(&ciInfo);
      if ( pPacket == NULL ) throw E_OUTOFMEMORY;

 

      // Build SOAP:Envelope
      CSOAPEnvelope* pEnvelope = pPacket->GetEnvelope();
      long iCookie = NULLNSCOOKIE;
      USES_CONVERSION;
      HRESULT hr = pEnvelope->InsertNamespace(std::string(W2A(m_bstrMethodURI)), 
                                              std::string("m"),
                                              &iCookie);
      if ( FAILED(hr) ) throw hr;

 

      // Build SOAP:Body and SOAP:MethodResponse
      CSOAPBody* pBody = pEnvelope->GetBody();
      CSOAPIndElement* pMethodElement = NULL;
      std::string strMethodResponse(std::string(W2A(m_bstrMethod) +
                                    std::string("Response")));
      hr = pBody->InsertElement(strMethodResponse,
                                iCookie,
                                &pMethodElement);
      if ( FAILED(hr) ) throw hr;

 

      // Build SOAP:MethodResponse <return> param
      // (which is the system time string)
      CSOAPEmbeddedElement* pEmbedded = NULL;
      hr = pMethodElement->InsertElement(std::string("return"),
                                         NULLNSCOOKIE,
                                         &pEmbedded);
      if ( FAILED(hr) ) throw hr;
      pEmbedded->SetElementData(strReturn);

 

      // Write XML Response payload to stream (without HTTP headers)
      std::ostrstream ostr;
      hr = pPacket->WriteToStream(ostr, true);

 

      // Get XML (SOAP) document
      CComBSTR bstrResponse(ostr.pcount()+1,ostr.str());
      ostr.freeze(0); // allows deletion of character stream upon destruction

 

      // Place XML in DOM
      VARIANT_BOOL bSuccess = false;
      hr = pResponse->loadXML(bstrResponse,&bSuccess);
      if ( hr == S_FALSE ) {
         // Assume memory problem...if this is the case,
         // then we won't be able to send a fault back
         // either. We'll deal with that issue in the
         // catch block.
         throw E_OUTOFMEMORY;
      } // if
   } // try
   catch(...) {
      // Create fault packet...this may have happened because
      // we couldn't load the XML document into the parser,
      // so this call will also result in failure. If so,
      // the returned XML DOM object will have no XML
      // document, and the caller will have to assume something
      // catastrophic happened...
      CreateFault(pResponse,E_UNEXPECTED);
   } // catch

 

   // Clean up...
   if ( pPacket != NULL ) delete pPacket;
}

It is in CreateResponse() that we utilize the reusable C++ SOAP objects (we'll also use them in the client, as you'll see). The method begins by creating a dummy SOAP HTTP packet. The SOAP objects will create for you a true HTTP packet, including the HTTP headers if you desire. In this case, IIS will create the packet for us so we can simply create a dummy SOAP HTTP object. We need at least a dummy object because the SOAP HTTP object contains the SOAP envelope object we'll need to access. We create the dummy SOAP HTTP object by completing a SOAPCALLINFO structure and passing it as a constructor argument to the SOAP HTTP object:

// Build SOAP HTTP packet (only used as a container
// for the Envelope)
SOAPCALLINFO ciInfo;
ciInfo.strHost = "none";
ciInfo.strPostHdr = "none";
ciInfo.strMethod = "none"; // won't be used for return packet
pPacket = new CSOAPHttpPacket(&ciInfo);
if ( pPacket == NULL ) throw E_OUTOFMEMORY;

With a SOAP HTTP object in hand, we obtain a copy of the SOAP envelope object and insert the namespace for the method's return. In this case, I've coded the namespace prefix to be 'm', which matches the client, but you could be more descriptive if you wish. Note a cookie, a generic 32-bit value, identifies the namespace once it is inserted. You'll later provide this cookie to child element objects that are similarly namespace-delimited, if any:

// Build SOAP:Envelope
CSOAPEnvelope* pEnvelope = pPacket->GetEnvelope();
long iCookie = NULLNSCOOKIE;
USES_CONVERSION;
HRESULT hr = pEnvelope->InsertNamespace(std::string(W2A(m_bstrMethodURI)), 
                                        std::string("m"),
                                        &iCookie);
if ( FAILED(hr) ) throw hr;

For this example we don't require a SOAP header. The reason for this is the SOAP header is available to provide a place to return meta-information, and in this case we have none. If the originating method call had an associated transaction identifier, for example, we would probably return that identifier with the transacted results of the call in the response header. Since there is no such meta-information required in this particular case, we jump right to the creation of the SOAP body and SOAP method response elements:

// Build SOAP:Body and SOAP:MethodResponse
CSOAPBody* pBody = pEnvelope->GetBody();
CSOAPIndElement* pMethodElement = NULL;
std::string strMethodResponse(std::string(W2A(m_bstrMethod) +
                              std::string("Response")));
hr = pBody->InsertElement(strMethodResponse,
                          iCookie,
                          &pMethodElement);
if ( FAILED(hr) ) throw hr;

Here, we request a pointer to the body object from the envelope object and insert a newly-created independent element that will represent the method response. This independent element will contain the return value and any parameter data to be returned to the caller (traditionally known as out data). GetRemoteTime() has no out parameters but does return a string value representing the time.

We then must insert a new element into the method response element that represents the return value:

// Build SOAP:MethodResponse <return> param
// (which is the system time string)
CSOAPEmbeddedElement* pEmbedded = NULL;
hr = pMethodElement->InsertElement(std::string("return"),
                                   NULLNSCOOKIE,
                                   &pEmbedded);
if ( FAILED(hr) ) throw hr;
pEmbedded->SetElementData(strReturn);

The resulting time string is itself stuffed into the <return /> SOAP XML element, which is itself encapsulated in the method response element, as you saw previously.

The next action we need to take is to get the SOAP XML data from the SOAP objects and into the XML parser so we can transmit the data back to the client. The SOAP objects are pure C++ objects, so the XML data is streamed out using a C++ stream. With the data in the stream, we can do nearly anything with it. In this case, I save the stream's contents to a BSTR, which happens to be wrapped in an ATL CComBSTR object:

// Write XML Response payload to stream (without HTTP headers)
std::ostrstream ostr;
hr = pPacket->WriteToStream(ostr, true);

 

// Get XML (SOAP) document
CComBSTR bstrResponse(ostr.pcount()+1,ostr.str());
ostr.freeze(0); // allows deletion of character stream upon destruction

With the data in the BSTR, I load the XML information into the XML parser:

// Place XML in DOM
VARIANT_BOOL bSuccess = false;
hr = pResponse->loadXML(bstrResponse, &bSuccess);
if ( hr == S_FALSE ) {
   // Assume memory problem...if this is the case,
   // then we won't be able to send a fault back
   // either. We'll deal with that issue in the
   // catch block.
   throw E_OUTOFMEMORY;
} // if

The reason I load the SOAP XML data into the parser rather than return it directly as a BSTR or an STL string is that the parser serves as a final check to be sure the XML data is well-formed. If the XML data isn't well-formed the parser will refuse to load the information. The "out of memory" assumption is a gross one, given that the XML could be mal-formed, but the result of this is the client is informed the server sustained an error, even if the root of the error may be imprecise. The fact the client is informed there was an error at all is what was most important to me in this case. Even so, you could bolster the error handling here by obtaining the XML parser's error object and cracking it to see more precisely what went wrong.

You may wonder what CreateFault() is there to do given the existence of the CreateResponse() function. CreateResponse() will build a SOAP packet with valid response information. CreateFault(), on the other hand, is specifically used to create SOAP fault packets. I use CreateFault() where I need to return errors to the client. But since the code for CreateFault() is similar to CreateResponse(), I won't examine it in detail here.

At this point, you have to refer all of the way back to Listing 1 to see what happens to the XML data you just created:

// Retrieve the XML response
if ( !pResponse.p ) throw "Client.ResponseError#Unable to retrieve XML/SOAP response from the XML parser";
CComBSTR bstrXML;
hr = pResponse->get_xml(&bstrXML);
if ( FAILED(hr) ) throw "Client.ResponseError#Unable to retrieve XML/SOAP response from the XML parser";

 

// Check the length of the result...if it is an empty
// string, something horrid happened and we need to
// return a (hardcoded) SOAP fault from here.
if ( !bstrXML.Length() ) throw "Client.ResponseError#Unable to retrieve XML/SOAP response from the XML parser";

 

// Send the response to the client
ULONG bufLen = bstrXML.Length();
USES_CONVERSION;
lpECB->WriteClient(lpECB->ConnID, (void*)W2A(bstrXML), &bufLen, HSE_IO_SYNC);

All but the last line merely retrieve the XML data and perform some rudimentary checks. The last line, however, passes the XML data back to the client. At this point you've delegated the return of the resulting SOAP information to IIS, which passes it back to the client.

Installing and Debugging ISAPI Extensions

Before I move to the client code, I should briefly describe how you install and test an ISAPI extension. The reason this is a bit more involved than a simple application is that ISAPI extensions are DLLs that reside in IIS's address space. Therefore, you have to debug through IIS. Visual Studio gives you this capability, as you'll see, but you need to make sure things are set up correctly.

To install your ISAPI extension, you must create a virtual directory from within the IIS administration tool Internet Information Services. I called my virtual directory VCDSoap. The naming conventions are the same as for actual directories. This next sentence is very important! When you create the virtual directory, be sure to indicate the security permissions should be such that external clients can invoke and execute scripts and that you want low application protection (you don't want the DLL run in a separate process but under IIS itself). Copy the ISAPI extension DLL to the actual directory that is associated with the virtual directory. Your extension is now installed.

To debug the extension, you must do two things in addition to compiling it as debug-enabled code. First, you must force IIS to load the extension DLL into memory. If you don't, you can't set breakpoints. To do this, navigate to the extension's URL using your Web browser. You may have noted I had code in place in my extension that responded to an HTTP GET method by providing a short textual message. This isn't required, but at least I know the extension is truly loaded and responding. With the DLL in memory, you must then use Visual Studio to invoke the remote process debugger.

To do this, bring up a copy of Visual Studio and select Start Debug from the Build menu item. This will bring up a dialog box with a list control that contains all of the processes currently active on your computer. Since IIS is a system process, you must check the checkbox for system processes and then look for inetinfo (IIS' process name). Double-click this and Visual Studio will begin debugging through IIS.

Now, load a copy of your extension's source code file(s) into Visual Studio and set a breakpoint. Assuming all went well, you should now be able to run a client application and have the breakpoint trip when IIS executes to that point. From there, you single-step or otherwise debug the extension code in the normal manner.

If you decide to make changes to the extension and try again, simply copy the new DLL over the existing DLL IIS was using. If for some reason you can't overwrite the file (it was marked as busy by the system), then you'll need to log out or even reboot and then copy the file. This part is a hassle, but it's still worth it to be able to debug your extension DLLs.

The SOAP Client

Now that you've seen the server-side SOAP code, you'll note the client-side code isn't very different. I created the MFC-based client you see in Figure 3. The client application is typical MFC-based user-interface code. You can make a single request, or you can alternatively poll the server for the time in 250ms intervals. Note the edit control contains the URL of the server's ISAPI extension, so be sure to type in the correct URL or you'll receive an error (typically "server not found").

There is a helper function in the MFC code that is interesting to look into. The reason for this is two-fold. First, IIS managed all of the server-side communication for us. We have to tackle that ourselves from the client. And second, we need to extract the returned time string from the SOAP data and reformat it for display or we need to process a fault. The helper function I use to do this is called MakeRequest() and is found in the VCDSoapClientDlg.cpp file, a listing of which you see in Listing 4.

Listing 4. The Client's makeRequest() Method

BOOL CVCDSoapClientDlg::MakeRequest() 
{
   BOOL bSucceeded = FALSE;
   CSOAPHttpPacket* pPacket = NULL;
   try {
      // Create a SOAP packet
      SOAPCALLINFO ciInfo;
      ciInfo.strHost = m_strHost;
      ciInfo.strPostHdr = "VCDSoapRequest";
      ciInfo.strActionData = "www.yourhosthere.com#GetRemoteTime";
      ciInfo.strMethod = "GetRemoteTime";
      pPacket = new CSOAPHttpPacket(&ciInfo);

 

      // Retrieve the envelope
      CSOAPEnvelope* pEnvelope = pPacket->GetEnvelope();
      if ( !pEnvelope ) throw IDS_E_NOENVELOPE;

 

      // Enable the standard stuff...
      pEnvelope->EnableStdAttribute(ENCATTRCOOKIE,true);

 

      // Insert the namespace...
      long iCookie = NULLNSCOOKIE;
      HRESULT hr = pEnvelope->InsertNamespace(std::string("www.yourhosthere.com"),
                                              std::string("m"),
                                              &iCookie);
      if ( FAILED(hr) ) throw IDS_E_NOENVELOPENS;

 

      // Next insert the method's independent element into the
      // body
      CSOAPBody* pBody = pEnvelope->GetBody();
      CSOAPIndElement* pMethodElement = NULL;
      hr = pBody->InsertElement(ciInfo.strMethod,iCookie,&pMethodElement);
      if ( FAILED(hr) ) throw IDS_E_NOINSMETHOD;

 

      // Fire off the message
      CComBSTR bstrResponse;
      if ( SUCCEEDED(SendReceive(pPacket,bstrResponse)) ) {
         // Stuff the response into an instance of the XML parser...
         CComPtr<IXMLDOMDocument> spResponseXMLDoc;
         hr = spResponseXMLDoc.CoCreateInstance(__uuidof(DOMDocument));
         if ( FAILED(hr) ) throw hr;

 

         VARIANT_BOOL bSuccess = false;
         hr = spResponseXMLDoc->loadXML(bstrResponse,&bSuccess);
         if ( FAILED(hr) ) throw IDS_E_NORESPONSE;
         if ( !bSuccess ) throw IDS_E_NORESPONSE;

 

         // Pull the time string by looking for a particular
         // node (the correctly-formatted response node):
         //
         // Check for <SOAP-ENV:Envelope>
         // <SOAP-ENV:Body>
         // <m:ShowTimeResponse>
         // <return>...
         //
         // Construct search string
         // "SOAP-ENV:Envelope/SOAP-ENV:Body/m:ShowTimeResponse/return"
         std::string strSS("SOAP-ENV:Envelope/SOAP-ENV:Body/");
         strSS += _T("m:");
         strSS += _T("GetRemoteTimeResponse");
         strSS += _T("/return");
         CComBSTR bstrSS(strSS.c_str());
         CComPtr<IXMLDOMNode> spResponse;
         hr = spResponseXMLDoc->selectSingleNode(bstrSS,&spResponse);
         if ( FAILED(hr) ) throw IDS_E_NORESPONSE;
         if ( spResponse.p != NULL ) {
            // Pull the node's value
            CComVariant var;
            hr = spResponse->get_nodeTypedValue(&var);
            if ( FAILED(hr) ) throw IDS_E_NORESPONSE;

 

            // Assign the value
            USES_CONVERSION;
            CComBSTR bstrVal(var.bstrVal);
            LPTSTR strVal = W2T(bstrVal);
            // Show the result...let's parse out some of
            // the information and rearrange to make a 
            // more user-friendly display...
            LPTSTR strYear = _tcstok(strVal,_T("-"));
            LPTSTR strMonth = _tcstok(NULL,_T("-"));
            LPTSTR strDay = _tcstok(NULL,_T("T"));
            LPTSTR strTime = _tcstok(NULL,_T("-+\0"));
            LPTSTR strBias = _tcstok(NULL,_T("\0"));
            m_strResult.Format("%s, %s/%s/%s (%s)",strTime,strMonth,strDay,strYear,strBias!=NULL?strBias:_T("UTC"));
            UpdateData(FALSE);
            // Set return value
            bSucceeded = TRUE;
         } // if
         else {
            // SOAP fault? Let's parse it and see...
            // Check for <SOAP-ENV:Envelope>
            // <SOAP-ENV:Body>
            // <SOAP-ENV:Fault>
            //
            // Construct search string
            // "SOAP-ENV:Envelope/SOAP-ENV:Body/SOAP-ENV:Fault"
            std::string strSS("SOAP-ENV:Envelope/SOAP-ENV:Body/SOAP-ENV:Fault");
            CComBSTR bstrSS(strSS.c_str());
            CComPtr<IXMLDOMNode> spFaultNode;
            hr = spResponseXMLDoc->selectSingleNode(bstrSS,&spFaultNode);
            if ( FAILED(hr) ) throw IDS_E_NORESPONSE;
            if ( spFaultNode.p != NULL ) {
               // Pull the faultcode's value
               strSS = "faultcode";
               bstrSS = strSS.c_str();
               CComPtr<IXMLDOMNode> spFaultCodeNode;
               CComVariant varFaultCode;
               hr = spFaultNode->selectSingleNode(bstrSS,&spFaultCodeNode);
               if ( FAILED(hr) ) throw IDS_E_NORESPONSE;
               if ( spFaultCodeNode.p != NULL ) {
                  // Get faultcode string
                  hr = spFaultCodeNode->get_nodeTypedValue(&varFaultCode);
                  if ( FAILED(hr) ) throw IDS_E_NORESPONSE;
               } // if

 

               // Pull the faultstring's value
               strSS = "faultstring";
               bstrSS = strSS.c_str();
               CComPtr<IXMLDOMNode> spFaultStringNode;
               CComVariant varFaultString;
               hr = spFaultNode->selectSingleNode(bstrSS,&spFaultStringNode);
               if ( FAILED(hr) ) throw IDS_E_NORESPONSE;
               if ( spFaultStringNode.p != NULL ) {
                  // Get faultstring string
                  hr = spFaultStringNode->get_nodeTypedValue(&varFaultString);
                  if ( FAILED(hr) ) throw IDS_E_NORESPONSE;
               } // if

 

               // Assign the values
               USES_CONVERSION;
               CComBSTR bstrFaultCode(varFaultCode.bstrVal);
               LPTSTR strFaultCode = W2T(bstrFaultCode);
               CComBSTR bstrFaultString(varFaultString.bstrVal);
               LPTSTR strFaultString = W2T(bstrFaultString);

 

               // Show the fault...
               LPTSTR strFault = _tcstok(strFaultCode,_T("."));
               LPTSTR strSubFault = _tcstok(NULL,_T("\0"));
               m_strResult = _T("(SOAP Fault)");
               UpdateData(FALSE);

 

               // Show the result
               CString strMsg;
               strMsg.Format("Fault: \"%s\"\nSubfault: \"%s\"\nFault string: \"%s\"",strFault,strSubFault,strFaultString);
               MessageBox(strMsg,"SOAP Fault Error",MB_OK|MB_ICONERROR);
            } // if
         } // else
      } // if
   } // try
   catch(int iErrIndex) {
      // Use the provided error index as a string table
      // lookup offset
      CString strMsg;
      strMsg.LoadString(iErrIndex);
      MessageBox(strMsg,"SOAP Application Error",MB_OK|MB_ICONERROR);
   } //catch
   catch(...) {
      // Generic error
      MessageBox("Error sending and/or receiving SOAP information","SOAP Application Error",MB_OK|MB_ICONERROR);
   } // catch

 

   // Clean up...
   if ( pPacket != NULL ) {
      delete pPacket;
      pPacket = NULL;
   } // if

 

   return bSucceeded;
}

Assuming the request was created and sent to the server (in SendReceive(), which we'll examine in a moment), much of the code you see in Listing 4 should now be familiar. We construct the SOAP packet on the client in the same way we do on the server. If the resulting time string comes back from the server, that is, if there is no fault, we extract the time string and reformat:

// Pull the time string by looking for a particular
// node (the correctly-formatted response node):
//
// Check for <SOAP-ENV:Envelope>
// <SOAP-ENV:Body>
// <m:ShowTimeResponse>
// <return>...
//
// Construct search string
// "SOAP-ENV:Envelope/SOAP-ENV:Body/m:ShowTimeResponse/return"
std::string strSS("SOAP-ENV:Envelope/SOAP-ENV:Body/");
strSS += _T("m:");
strSS += _T("GetRemoteTimeResponse");
strSS += _T("/return");
CComBSTR bstrSS(strSS.c_str());
CComPtr<IXMLDOMNode> spResponse;
hr = spResponseXMLDoc->selectSingleNode(bstrSS,&spResponse);
if ( FAILED(hr) ) throw IDS_E_NORESPONSE;
if ( spResponse.p != NULL ) {
   // Pull the node's value
   CComVariant var;
   hr = spResponse->get_nodeTypedValue(&var);
   if ( FAILED(hr) ) throw IDS_E_NORESPONSE;

 

   // Assign the value
   USES_CONVERSION;
   CComBSTR bstrVal(var.bstrVal);
   LPTSTR strVal = W2T(bstrVal);

 

   // Show the result...let's parse out some of
   // the information and rearrange to make a 
   // more user-friendly display...
   LPTSTR strYear = _tcstok(strVal,_T("-"));
   LPTSTR strMonth = _tcstok(NULL,_T("-"));
   LPTSTR strDay = _tcstok(NULL,_T("T"));
   LPTSTR strTime = _tcstok(NULL,_T("-+\0"));
   LPTSTR strBias = _tcstok(NULL,_T("\0"));
   m_strResult.Format("%s, %s/%s/%s (%s)",strTime,strMonth,strDay,strYear,strBias!=NULL?strBias:_T("UTC"));
   UpdateData(FALSE);

 

   // Set return value
   bSucceeded = TRUE;
} // if

The trick is to create a search string that contains the XML node's name and look for the node. If we find the node, we retrieve its typed value (which will always be a string because SOAP returns data as XML text) and convert the time information we find there. The resulting time string is then handed to the dialog box and displayed. Fault processing isn't vastly different, although I do present the error in the form of a message box.

SendReceive() is interesting because it is here you actually submit the request to the server and await the response. This implementation blocks while you wait for the response, but it need not be so (the XML parser can be invoked as a free-threaded COM object, in which case you can spawn a thread to manage the communications for you). The secret here is to use the HTTP object that ships with the XML parser. It makes your HTTP work on the client-side very easy.

Listing 5. The Client's SendReceive() Method

HRESULT CVCDSoapClientDlg::SendReceive(CSOAPHttpPacket* pPacket, CComBSTR& bstrResponse)
{
   // Check the pointer
   if ( pPacket == NULL ) return E_POINTER;
   HRESULT hr = S_OK;
   try {
      // Create the HTTP request object
      CComPtr<IXMLHttpRequest> spRequest;
      hr = spRequest.CoCreateInstance(__uuidof(XMLHTTPRequest));
      if ( FAILED(hr) ) throw hr;

 

      // Open the connection to the remote host
      std::string strHost;
      pPacket->GetHostText(strHost);
      hr = spRequest->open(CComBSTR("POST"),
                           CComBSTR(strHost.c_str()),
                           CComVariant(FALSE),
                           CComVariant(VT_BSTR),
                           CComVariant(VT_BSTR));
      if ( FAILED(hr) ) throw hr;

 

      // Set up the HTTP headers
      std::string strData;
      pPacket->GetPostText(strData);
      hr = spRequest->setRequestHeader(CComBSTR("POST"),CComBSTR(strData.c_str()));
      if ( FAILED(hr) ) throw hr;

 

      hr = spRequest->setRequestHeader(CComBSTR("HOST"),CComBSTR(strHost.c_str()));
      if ( FAILED(hr) ) throw hr;

 

      pPacket->GetContentTypeText(strData);
      hr = spRequest->setRequestHeader(CComBSTR("Content-Type"),CComBSTR(strData.c_str()));
      if ( FAILED(hr) ) throw hr;

 

      pPacket->GetSoapActionText(strData);
      hr = spRequest->setRequestHeader(CComBSTR("SOAPAction"),CComBSTR(strData.c_str()));
      if ( FAILED(hr) ) throw hr;

 

      // Create an instance of the XML parser, which is used to
      // ship the SOAP document to the remote host.
      CComPtr<IXMLDOMDocument> spRequestXMLDoc;
      hr = spRequestXMLDoc.CoCreateInstance(__uuidof(DOMDocument));
      if ( FAILED(hr) ) throw hr;

 

      // Get XML (SOAP) document
      std::ostrstream ostr;
      pPacket->WriteToStream(ostr,true);
      CComBSTR bstrRequest(ostr.pcount()+1,ostr.str());
      ostr.freeze(0); // allows deletion of character stream upon destruction
      VARIANT_BOOL bSuccess = false;
      hr = spRequestXMLDoc->loadXML(bstrRequest,&bSuccess);
      if ( FAILED(hr) ) throw hr;
      if ( !bSuccess ) throw E_UNEXPECTED;

 

      // Send the information to the remote host
      CComQIPtr<IDispatch> spXMLDisp;
      spXMLDisp = spRequestXMLDoc;
      hr = spRequest->send(CComVariant(spXMLDisp));
      if ( FAILED(hr) ) throw hr;

 

      // Get the status code from the response
      long iStatus = 0;
      hr = spRequest->get_status(&iStatus);
      if ( FAILED(hr) ) throw hr;

 

      // Check the status code for "200"
      if ( iStatus != 200 ) {
         // An HTTP error...
         throw iStatus;
      } // if

 

      // Get the response as a string and return it
      hr = spRequest->get_responseText(&bstrResponse);
      if ( FAILED(hr) ) throw hr;
   } // try
   catch(HRESULT hrErr) {
      // Bad HRESULT
      CComPtr<IErrorInfo> spIErrorInfo;
      hr = GetErrorInfo(0,&spIErrorInfo);
      if ( spIErrorInfo.p != NULL ) {
         CComBSTR bstrDesc;
         spIErrorInfo->GetDescription(&bstrDesc);
         USES_CONVERSION;
         MessageBox(W2T(bstrDesc),"SOAP Application Error",MB_OK|MB_ICONERROR);
      } // if
      else {
         // No error object, so fake it...
         MessageBox("Error processing the SOAP request/response","SOAP Application Error",MB_OK|MB_ICONERROR);
      } // else
      hr = hrErr;
   } // catch
   catch(...) {
      // Generic error
      MessageBox("Unknown error processing the SOAP request/response","SOAP Application Error",MB_OK|MB_ICONERROR);
   } // catch

 

   return hr;
}

SendReceive() begins by creating an instance of the XML HTTP object:

// Create the HTTP request object
CComPtr<IXMLHttpRequest> spRequest;
hr = spRequest.CoCreateInstance(__uuidof(XMLHTTPRequest));
if ( FAILED(hr) ) throw hr;

It then fills in the HTTP headers, including the SOAPAction header:

// Open the connection to the remote host
std::string strHost;
pPacket->GetHostText(strHost);
hr = spRequest->open(CComBSTR("POST"),
                     CComBSTR(strHost.c_str()),
                     CComVariant(FALSE),
                     CComVariant(VT_BSTR),
                     CComVariant(VT_BSTR));
if ( FAILED(hr) ) throw hr;

 

// Set up the HTTP headers
std::string strData;
pPacket->GetPostText(strData);
hr = spRequest->setRequestHeader(CComBSTR("POST"),CComBSTR(strData.c_str()));
if ( FAILED(hr) ) throw hr;

 

hr = spRequest->setRequestHeader(CComBSTR("HOST"),CComBSTR(strHost.c_str()));
if ( FAILED(hr) ) throw hr;

 

pPacket->GetContentTypeText(strData);
hr = spRequest->setRequestHeader(CComBSTR("Content-Type"),CComBSTR(strData.c_str()));
if ( FAILED(hr) ) throw hr;

 

pPacket->GetSoapActionText(strData);
hr = spRequest->setRequestHeader(CComBSTR("SOAPAction"),CComBSTR(strData.c_str()));
if ( FAILED(hr) ) throw hr;

With the HTTP headers specified in the XML HTTP object, we can now actually send the XML data. To do this, we load a copy of the XML document into the parser and query the parser object for its IDispatch interface. We need the IDispatch interface because the XML HTTP object requires it. With the IDispatch interface pointer in hand, we actually send the data:

VARIANT_BOOL bSuccess = false;
hr = spRequestXMLDoc->loadXML(bstrRequest,&bSuccess);
if ( FAILED(hr) ) throw hr;
if ( !bSuccess ) throw E_UNEXPECTED;

 

// Send the information to the remote host
CComQIPtr<IDispatch> spXMLDisp;
spXMLDisp = spRequestXMLDoc;
hr = spRequest->send(CComVariant(spXMLDisp));
if ( FAILED(hr) ) throw hr;

When the send() method completes, the XML HTTP object will contain a copy of the response document from the server. Assuming no unforeseen catastrophic errors, we next check the status of the returned HTTP packet. We're looking for a "200" status, which indicates things went well from a communications perspective. We still might have a SOAP fault, but at least the data went to the server and the server responded at the pure communications level:

// Get the status code from the response
long iStatus = 0;
hr = spRequest->get_status(&iStatus);
if ( FAILED(hr) ) throw hr;

 

// Check the status code for "200"
if ( iStatus != 200 ) {
   // An HTTP error...
   throw iStatus;
} // if

Keep in mind that the SOAP specification isn't clear regarding the use of the HTTP 200 response and a SOAP fault. While I believe this is the most appropriate implementation, the authors of the SOAP specification may provide further guidance and tell us that a SOAP fault is expected to be returned in a 500-level HTTP error packet.

Finally, the returned document is extracted from the XML HTTP object and returned for further processing, such as you saw in Listing 3:

// Get the response as a string and return it

hr = spRequest->get_responseText(&bstrResponse);

if ( FAILED(hr) ) throw hr;

With the resulting SOAP XML document returned to the client for processing, you have seen a complete implementation of a SOAP processing architecture, from client to server and back again to the client. Hopefully you'll find some of this code interesting and reusable in your own applications should you elect to implement the SOAP functionality yourself (versus choosing an off-the-shelf implementation). In any case, even if you do use a third party SOAP processor, hopefully you have a deeper appreciation for how the SOAP processor works and what must take place for successful SOAP remote method calls to transpire.

Comments? Questions? Find a bug? Please send me a note!


[Back] [Left Arrow] [Right Arrow][Home]