Example Code : WebServices Core + CFNetwork for SOAP + HTTP auth on OS X

WebServices Core is Apple’s C-based framework for invoking and implementing SOAP and XML-RPC based webservices. It’s a bit low level, but I find it to be a great tool. When combined with CFNetwork (Apple’s C-based framework for networking — including HTTP), it can be used to access SOAP-based webservices that require HTTP Basic and Digest authentication.

WebServices Core’s biggest problem is its lack of documentation and example code. There’s some, but not enough.

So I’d like to make a modest offering to the Google sphere — an example of using WebServices Core + CFNetwork’s HTTP auth support to access a SOAP service that requires HTTP Basic auth.

Download Sample Code Xcode Project (16k).

Let’s walk you thru it:

First, you need your SOAP request settings, including SOAP method, method namepsace, parameters, and SOAPAction HTTP header. I’m working against a local Tomcat webserver instance on which I’ve setup an Axis2-based SOAP service behind HTTP Basic auth.


// SOAP call settings
NSURL *url = [NSURL URLWithString:@"http://localhost:8080/axis2/EchoHeaders.jws"];
NSString *method = @"echo";
NSString *namespace = @"http://localhost:8080/axis2/EchoHeaders.jws";

// SOAP call params
NSDictionary *params = [NSDictionary dictionaryWithObject:@"foobar"
                                                   forKey:@"param"];
NSArray *paramOrder = [NSArray arrayWithObject:@"param"];

// SOAP call http headers -- some SOAP server impls require even empty SOAPAction headers
NSDictionary *reqHeaders = [NSDictionary dictionaryWithObject:@"" forKey:@"SOAPAction"];

The SOAP method I’m calling is a very simple method that simply echos the single SOAP request parameter back in the SOAP response. The request parameter’s name is “param”. Here, I’m sending a value of “foobar” which will be echoed back in the response.

I like to use Cocoa/ObjC objects for all of my working data, and take advantage of Apple’s CFTypeRef-to-Cocoa object toll-free-bridging as necessary. Working with ObjC objects is just easier.

Now create a SOAP call object — represented by a WSMethodInvocationRef.


// create SOAP call
WSMethodInvocationRef soapCall = createSOAPCall(url, method, namespace, params, paramOrder, reqHeaders);

As you can see, I have a custom function, createSOAPCall that actually creates the WSMethodInvocationRef object. Here it is:


WSMethodInvocationRef createSOAPCall(NSURL *url,
                                     NSString *method,
                                     NSString *namespace,
                                     NSDictionary *params,
                                     NSArray *paramOrder,
                                     NSDictionary *reqHeaders)
{
    WSMethodInvocationRef soapCall = WSMethodInvocationCreate((CFURLRef)url,
                                                              (CFStringRef)method,
                                                              kWSSOAP2001Protocol);

    // set SOAP params
    WSMethodInvocationSetParameters(soapCall, (CFDictionaryRef)params, (CFArrayRef)paramOrder);

    // set method namespace
    WSMethodInvocationSetProperty(soapCall, kWSSOAPMethodNamespaceURI, (CFStringRef)namespace);

    // Add HTTP headers (with SOAPAction header) -- some SOAP impls require even empty SOAPAction headers
    WSMethodInvocationSetProperty(soapCall, kWSHTTPExtraHeaders, (CFDictionaryRef)reqHeaders);

    // for good measure, make the call follow redirects.
    WSMethodInvocationSetProperty(soapCall,    kWSHTTPFollowsRedirects, kCFBooleanTrue);

    // set debug props
    WSMethodInvocationSetProperty(soapCall, kWSDebugIncomingBody,     kCFBooleanTrue);
    WSMethodInvocationSetProperty(soapCall, kWSDebugIncomingHeaders, kCFBooleanTrue);
    WSMethodInvocationSetProperty(soapCall, kWSDebugOutgoingBody,     kCFBooleanTrue);
    WSMethodInvocationSetProperty(soapCall, kWSDebugOutgoingHeaders, kCFBooleanTrue);

    return soapCall;
}

I believe this is pretty self-explanatory. Here we create the SOAP call object and configure all of its settings and parameters.

Now it’s time to invoke the SOAP call and grab the HTTP response. We need to grab the HTTP response directly if we want to respond to HTTP auth challenges:


// invoke SOAP call
NSDictionary *result = (NSDictionary *)WSMethodInvocationInvoke(soapCall);

// get HTTP response from SOAP call so we can see response HTTP status code
CFHTTPMessageRef res = (CFHTTPMessageRef)[result objectForKey:(id)kWSHTTPResponseMessage];
int resStatusCode = CFHTTPMessageGetResponseStatusCode(res);

Now check for an HTTP response code of 401 or 407 which would signal an HTTP auth challenge:


// if we got an auth challenge, attempt to add atuh creds
while (401 == resStatusCode || 407 == resStatusCode) {

If there was an auth challenge, you must:

  1. Grab the HTTP response headers from the first SOAP call response and extract the auth challenge information
  2. Create a CFHTTMessageRef to represent an HTTP request
  3. Add auth credentials to the request to match the auth challenge info you found in the SOAP response
  4. Associate the newly-created HTTP request (with auth creds) with a new SOAP call
  5. Invoke the new SOAP call

CFHTTPAuthenticationRef auth = CFHTTPAuthenticationCreateFromResponse(kCFAllocatorDefault, res);
NSString *scheme = [(NSString *)CFHTTPAuthenticationCopyMethod(auth) autorelease];
NSString *realm  = [(NSString *)CFHTTPAuthenticationCopyRealm(auth)  autorelease];
NSArray *domains = [(NSArray *)CFHTTPAuthenticationCopyDomains(auth) autorelease];

NSLog(@"Providing auth info for scheme: %@\n, realm: %@\n, domains: %@",
      scheme, realm, domains);

NSString *username = @"tomcat";
NSString *password = @"tomcat";

// create custom http request with auth creds
NSString *reqMethod = @"POST";
CFHTTPMessageRef req = CFHTTPMessageCreateRequest(kCFAllocatorDefault,
                                                  (CFStringRef)reqMethod,
                                                  (CFURLRef)url,
                                                  kCFHTTPVersion1_1);

// add auth creds to request.
Boolean success = CFHTTPMessageAddAuthentication(req,
                                                 res,
                                                 (CFStringRef)username,
                                                 (CFStringRef)password,
                                                 NULL,
                                                 false);
if (!success) {
    NSLog(@"failed to add auth to request");
    return EXIT_FAILURE;
}

// create a new SOAP call
soapCall = createSOAPCall(url, method, namespace, params, paramOrder, reqHeaders);

// add HTTP request auth creds to SOAP call
WSMethodInvocationSetProperty(soapCall, kWSHTTPMessage, req);

Finally, invoke the new SOAP call, and inspect the reuslts:


// invoke SOAP call again
result = (NSDictionary *)WSMethodInvocationInvoke(soapCall);

NSLog(@"result: %@", result);

You can see the complete example by downloading the Xcode proj. Here’s hoping someone finds this useful someday.


About this entry