Enterprise developers are excited about the power of Service-Oriented Architectures (SOA) to simplify business-to-business communication, and many large internet sites, like Amazon.com, make their information available via Web services, often based on SOAP (the Simple Object Access Protocol). If you've ever wondered whether you can access these kinds of web services from the Mac, the answer is Yes, and this article shows you how.
The Web Services Core framework is Apple's C-based framework for invoking and implementing SOAP- and XML-RPC-based web services. CFNetwork is Apple's C-based framework for network programming, including HTTP. When combined, WebServices Core and CFNetwork can be used to access remote SOAP-based web services that require HTTP Basic or Digest authentication.
Below is an example of using WebServices Core plus CFNetwork's HTTP authentication features to access a remote SOAP service that requires HTTP Basic authentication.
Download Sample Code Xcode Project (.DMG, 456KB).
This example sends SOAP requests to a tiny Java-based SOAP server program that you can download and run locally on your machine. This simple test server implements only a single SOAP method (called echo
) behind HTTP Basic authentication. The test server challenges all SOAP requests, but allows any username and password.
To run the server:
- Download and unzip the the test server jar files:
Download Test SOAP Server Jar (.DMG, 444KB).
- Open a Terminal.app window and cd into the directory where the test server jar is located.
- Type the following command to run the test server on port 8888: >
java -jar SOAP_AuthExampleServer 8888
Begin by declaring or fetching the data that makes up the parameters and other details of the SOAP request. This includes the SOAP method name, method namespace, request parameters, request parameter order and SOAPAction HTTP header.
Listing 1: Declare SOAP request settings
// SOAP request settings NSURL *url = [NSURL URLWithString:@"http://localhost:8888/"]; NSString *method = @"echo"; NSString *namespace = @"http://localhost:8888/"; // SOAP request params NSDictionary *params = [NSDictionary dictionaryWithObject:@"foobar" forKey:@"param"]; NSArray *paramOrder = [NSArray arrayWithObject:@"param"]; // SOAP request http headers -- some SOAP server impls require even empty SOAPAction headers NSDictionary *reqHeaders = [NSDictionary dictionaryWithObject:@"" forKey:@"SOAPAction"];
If you are able to include the Foundation framework, it's often easier to work with Cocoa/Objective-C objects as shown in Listing 1, and cast the Cocoa objects to CoreFoundation types when necessary. This approach takes advantage of the CoreFoundation-to -Cocoa toll-free-bridging for basic data types, and makes many common tasks (such as memory management) easier.
Also included in Listing 1 are the declarations of the data used to construct the SOAP request. The SOAP method to be called is a simple method named echo
that takes a single string parameter named param
. This method echoes the given string parameter (foobar
in our example) in the SOAP response.
The URL for this example points to a tiny SOAP server running on port 8888 on localhost. Values for the method namespace and SOAPAction HTTP headers are also declared. Many SOAP server implementations require the presence of an empty SOAPAction header even if the method itself does not specifically require a header value. Creating a dictionary with an empty value for the SOAPAction
key and attaching it to the SOAP request forces an empty SOAPAction header to be sent along with the request.
Next, a SOAP request is created from the settings above. WebServices Core represents a SOAP request as aWSMethodInvocationRef
type, as shown in Listing 2.
Listing 2: Call function to create SOAP request
// create SOAP request WSMethodInvocationRef soapReq = createSOAPRequest(url, method, namespace, params, paramOrder, reqHeaders);
In Listing 3, creation of the SOAP request is delegated to the createSOAPRequest
function that returns aWSMethodInvocationRef
object.
Listing 3: Custom function to create SOAP request
WSMethodInvocationRef createSOAPRequest(NSURL *url, NSString *method, NSString *namespace, NSDictionary *params, NSArray *paramOrder, NSDictionary *reqHeaders) { WSMethodInvocationRef soapReq = WSMethodInvocationCreate((CFURLRef)url, (CFStringRef)method, kWSSOAP2001Protocol); // set SOAP params WSMethodInvocationSetParameters(soapReq, (CFDictionaryRef)params, (CFArrayRef)paramOrder); // set method namespace WSMethodInvocationSetProperty(soapReq, kWSSOAPMethodNamespaceURI, (CFStringRef)namespace); // Add HTTP headers (with SOAPAction header) -- some SOAP impls require even empty SOAPAction headers WSMethodInvocationSetProperty(soapReq, kWSHTTPExtraHeaders, (CFDictionaryRef)reqHeaders); // for good measure, make the request follow redirects. WSMethodInvocationSetProperty(soapReq, kWSHTTPFollowsRedirects, kCFBooleanTrue); // set debug props WSMethodInvocationSetProperty(soapReq, kWSDebugIncomingBody, kCFBooleanTrue); WSMethodInvocationSetProperty(soapReq, kWSDebugIncomingHeaders, kCFBooleanTrue); WSMethodInvocationSetProperty(soapReq, kWSDebugOutgoingBody, kCFBooleanTrue); WSMethodInvocationSetProperty(soapReq, kWSDebugOutgoingHeaders, kCFBooleanTrue); return soapReq; }
Note that in addition to creating the SOAP request, several debug properties of the WSMethodInvocationRef
are also set to true. This causes the raw XML contents of the SOAP request and response messages to be included in the result dictionary returned from executing the SOAP request. Viewing this raw XML can be extremely helpful in debugging SOAP client code.
The next step is to invoke the initial SOAP request. Do this using the WSMethodInvocationInvoke
function which returns a dictionary containing the SOAP response and other debug information.
Since the service accessed requires HTTP Basic authentication, expect the HTTP headers of the SOAP response to contain authentication challenge information. To respond to the authentication challenge, retrieve the HTTP response from the result dictionary using the kWSHTTPResponseMessage
key. The HTTP response is represented by aCFHTTMessageRef
object.
Listing 4: Create SOAP request
// invoke SOAP request NSDictionary *result = (NSDictionary *)WSMethodInvocationInvoke(soapReq); // get HTTP response from SOAP request so we can see response HTTP status code CFHTTPMessageRef res = (CFHTTPMessageRef)[result objectForKey:(id)kWSHTTPResponseMessage];
Now check for an HTTP response code of 401
or 407
that would signal an HTTP authentication challenge:
Listing 5: Check HTTP response status code
int resStatusCode = CFHTTPMessageGetResponseStatusCode(res); // if response status code indicates auth challenge, attempt to add atuh creds while (401 == resStatusCode || 407 == resStatusCode) {
If an authentication challenge is returned, you must:
- Extract the HTTP response headers from the first SOAP response. These headers contain the necessary auth challenge information.
- Create a new
CFHTTMessageRef
to represent a new HTTP request. - Gather username and password information by prompting the user or by querying some external data source.
- Combine the username and password with the auth challenge information from the initial SOAP response to create auth credentials.
- Attach the auth credentials to the newly-created HTTP request.
- Create a new SOAP request and combine it with the new HTTP request to produce a SOAP request with the necessary auth credentials attached.
- Invoke the new SOAP request.
Listing 6: Extract HTTP authentication challenge, and create new SOAP request with credentials
CFHTTPAuthenticationRef auth = CFHTTPAuthenticationCreateFromResponse(kCFAllocatorDefault, res); // extract details of the auth challenge to display // when prompting the user for username and password information NSString *scheme = [(NSString *)CFHTTPAuthenticationCopyMethod(auth) autorelease]; NSString *realm = [(NSString *)CFHTTPAuthenticationCopyRealm(auth) autorelease]; NSArray *domains = [(NSArray *)CFHTTPAuthenticationCopyDomains(auth) autorelease]; NSLog(@"Providing auth info for /nscheme: %@/n, realm: %@/n, domains: %@", scheme, realm, domains); // Replace with a user prompt or fetch data from remote source NSString *username = @"example"; NSString *password = @"example"; // 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 request soapReq = createSOAPRequest(url, method, namespace, params, paramOrder, reqHeaders); // add HTTP request auth creds to SOAP request WSMethodInvocationSetProperty(soapReq, kWSHTTPMessage, req);
Finally, send the new SOAP request, and inspect the results:
Listing 7: Send SOAP request and print results
// send SOAP request again result = (NSDictionary *)WSMethodInvocationInvoke(soapReq); NSLog(@"result: %@", result);
For More Information
ADC Reference Library: Web Services Core Programming Guide
Published: 2007-03-08