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.

11 Responses to “Example Code : WebServices Core + CFNetwork for SOAP + HTTP auth on OS X”

  1. John Chandler Says:

    The project fails to build, complaining about a missing “Release” build directory. A directory of that name exists, but is not a direct descendant of “build”. That is, Xcode seems to want “SOAP_AuthExample/build/Release”, but only “SOAP_AuthExample/SOAP_AuthExample.build/Release” exists. (Of course, this may not actually be the problem.)

    Is this a versioning problem with Xcode? I’m using 2.5. I didn’t see any indication that 3.0 was required though.

  2. Nik Says:

    Thanks a million, Todd. :-) Just a quick question (yupp, the question is quick, I expect the answer is kind of long-ish): how do you deal with complex types? I’ve used your SOAPclient to debug my services so I know you’ve got the answer. ;-) Looking forward to hearing about it. :-)

    Cheers

    Nik

  3. Cocoa and SOAP tutorial Says:

    [...] looking at the WebServices Core again, and this time I found that Todd Ditchendorf has written a nice tutorial about it. I’m working my way through it and it looks [...]

  4. Anyone get this working? Says:

    I have yet to get this working. I am having the same problem John Chandler is having. I can not get it to compile… I am running xcode 3.0 and still get a ton of compile errors from the code you (and apple) supply during linking. (22 errors).

    “_kWSDebugIncomingHeaders”, referenced from:_kWSDebugIncomingHeaders$non_lazy_ptr in SOAP_AuthExample.o

  5. Dion Says:

    Include CoreServices.framework in your project which contains the WebServicesCore.framework and you should be good to go.

  6. Sean Todd Says:

    Thanks for this Todd!

    It is really helpful. You are right, there isn’t much out there in documentation of Web Services. It is odd how much Apple has ignored this.

    There is also this PDF here: http://developer.apple.com/documentation/Networking/Conceptual/UsingWebservices/Web_Services.pdf
    It is a PDF version of most of what you linked to in the first link.

  7. Glenn Says:

    Todd, all I can say is…you rock!

    I don’t know if this example was created while you were at Apple; I see it was refined and published there a month later, at: http://developer.apple.com/internet/webservices/webservicescoreandcfnetwork.html.

    I lucked upon the Apple version first, but it’s pretty obvious that it would not be there except for your efforts.

    The Apple sample code web site has nothing, and my local /Developer dir has one example (XMethodInspector) but I cannot get it to build. (It seems like it needs to run WSMakeStubs, but it doesn’t, and I haven’t tried adding it myself.)

    The project on Apple’s site tries to build under 10.4 and I’m on 10.5, so first I got a bunch of errors because of that, and eventually I figured out that I needed to manually add CoreServices.framework to the project.

    Once I got a clean build, it was relatively smooth sailing, but that first build was a bear. I’m not complaining, though; again, without your efforts here, I’d still be lost.

  8. Glenn Says:

    P.S. I fibbed. The first batch of compile errors were because I needed to add CoreServices.framework, and then I realized I had to change the project from 10.4 to 10.5.

  9. John Says:

    Hi Todd,

    Being fairly new to mac programming I may have bitten off a little more than I can chew. I’ve worked through a few cocoa manuals and books, and have a limited knowledge of c programming. So I thought I’d start small and write a small app that interfaces with an api with soap. My only aim is to have it talk to a sports betting service and display a balance in a window. Simple little project I thought, until I started! lol.

    So thanks to your pointers, without which I would have given up a longtime back, I’ve got it to a point where it looks like it’s hitting the server, however I’m getting only garbage back that I can not make sense of.

    “/FaultCode” = -65794;
    “/FaultExtra” = {domain = -1; error = -65795; msg = “No valid XML data in response”; };
    “/FaultString” = “/CFStreamFault”;

    Can you offer any help on getting me started again? Any help appreciated. Am I missing something simple or am I being outrageously ambitious on a first project?

    Keep up the great work.

  10. Shane Kamar Says:

    I’m just using part of this… the part without that authentication, and It won’t build. It gives me an error about conflicting types on that custom function. This is my only hope (there’s almost no documentation) for using SOAP with cocoa. What do I do?

  11. John Says:

    Well, I managed to get to the bottom of my problem. Seems a simple typo was to blame. I’ve now got reams and reams of XML data coming in. Just now have to go figure how to make a call against it.

    Any suggestions?

Leave a Reply