Using SOAP web services on the iPhone

by tdavies on 1/14/09

One notable omission in the iPhone SDK is the lack of a Web Services framework like WebServicesCore from the OSX SDK. Given the iPhone’s emphasis on online connectivity, it is at least surprising that there is no native framework stack for SOAP web services. So, what’s a developer to do? You have to roll your own.

Although it is obviously easier and preferable to use a framework, interacting with SOAP without one is not that difficult; It’s similar to implementing a REST-ful web service: Data is POSTed over HTTP at a particular endpoint URL, and the application translates the results of that operation into native objects in its run-time.

A sample Omniture Api Soap Request

Here’s a SOAP request for a basic report — an overtime trend of one metric — using the Omniture Web Services API:

<soapenv:Envelope>

   <soapenv:Header>

      <wsse:Security>

         <wsse:UsernameToken>

            <wsse:Username>$(ApiUsername)</wsse:Username>

            <wsse:Password>$(OneTimeEncryptedPassword)</wsse:Password>

            <wsse:Nonce>$(OneTimeNonce)</wsse:Nonce>

            <wsu:Created>2009-01-09T18:53:56.545Z</wsu:Created>

         </wsse:UsernameToken>

      </wsse:Security>

   </soapenv:Header>

   <soapenv:Body>

      <omn:Report.QueueOvertime>

         <reportDescription xsi:type=“omn:reportDescription”>

            <reportSuiteID xsi:type=“xsd:string”>$(ReportSuiteId)</reportSuiteID>

            <date xsi:type=“xsd:string”>2008-11</date>

            <dateGranularity>day</dateGranularity>

            <metrics soapenc:arrayType=“omn:reportDefinitionMetric[]” xsi:type=“soapenc:Array”>

               <item>

                  <id>pageviews</id>

               </item>

            </metrics>

         </reportDescription>

      </omn:Report.QueueOvertime>

   </soapenv:Body>

</soapenv:Envelope>

This chunk of XML is POSTed to our SOAP endpoint, https://api.omniture.com/admin/1.2/. The response returned looks something like the following:

<?xml version=“1.0” encoding=“UTF-8”?>

<SOAP-ENV:Envelope>

   <SOAP-ENV:Body>

      <ns1:>

         <return xsi:type=tns:reportQueueResponse>

            <status xsi:type=“xsd:string”>queued</status>

            <statusMsg xsi:type=“xsd:string”>Your report has been queued</statusMsg>

            <reportID xsi:type=“xsd:int”>1313624</reportID>

         </return>

      </ns1:Report.QueueOvertimeResponse>

   </SOAP-ENV:Body>

</SOAP-ENV:Envelope>

 

Creating and Parsing a SOAP request on the iPhone

To get this request to work in an iPhone application, we need to do the following:

  1. Create the SOAP header, complete with valid authentication credentials
    1. Encrypt your Omniture api username and secret.
    2. Use [NSString stringWithFormat:@"..."] to create the XML for our SOAP request.
  2. Package the SOAP header from (1), along with a SOAP Body, into a SOAP envelope.
  3. POST the SOAP envelope to the Omniture Web Service endpoint.
  4. Store the results and parse them using libxml.

Authentication Credentials

The following code snippet creates a valid SOAP header in Objective-C:

/** Returns a SOAP Header that complies to the OASIS UsernameToken profile specification
 */
- (NSString*) createSoapHeader {
    NSString* formatString = @"...use the header sample provided earlier...";
    //nonce
    srand(time(NULL));
    int n = rand();
    NSString* nonce = [[NSString stringWithFormat:@"%i", n] md5HexHash];
    //binary version for base64 encoding
    NSData* nonceBinary = [nonce dataUsingEncoding:NSUTF8StringEncoding];
    //date
    NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"];
    NSString* created = [formatter stringFromDate:[[NSDate date] 
                                                    dateWithCalendarFormat:nil 
                                                    timeZone:[NSTimeZone 
                                                        timeZoneForSecondsFromGMT:0]]];
    [formatter release];
    // digest = base64(sha1(base64_decode(nonce)+created+secret))
    NSString* digest_concat = [NSString stringWithFormat:@"%@%@%@", 
                                            nonce, created, 
                                            @"YOUR_API_SECRET_THAT_YOU_NEVER_SHARE"];
    NSString* digest = [[[digest_concat dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO] 
                          sha1Hash] 
                        base64Encoding];
    return [NSString stringWithFormat:formatString, 
            [user username], 
            digest, 
            [nonceBinary base64Encoding], 
            created];
}

In summary, the function above takes your Api username, secret (which you never share over the wire), a randon number (“nonce”), and the current timestamp, and encrypts them to insure that they can’t be re-used or stolen by someone sniffing around. The technical details are embedded in the code above, and are explained in detail elsewhere.

There are several functions in use here that don’t come by default in Objective-C. You will need to include the excellent CocoaCryptHashing library for md5, and find a C version of a -base64encoding method. (Fortunately, all of this code, and how to link it, can be found in the iPhone code sample at http://developer.omniture.com/ ).

Posting the SOAP envelope

The full SOAP envelope can be created using [NSString stringWithFormat:@"..."]. Once created, get the data using NSUrlConnection:

NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:@"https://api.omniture.com/admin/1.2/"];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:SOAP_ENVELOPE_GOES_HERE];
[request setValue:@"https://api.omniture.com" forHTTPHeaderField:@"Host"];
[request setValue:@"text/xml; charset=UTF-8" forHTTPHeaderField:@"Content-type"];

NSURLResponse* response = nil;
NSError* error = nil;
NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];

The data object returned from the exercise above now needs to be parsed and handled by libxml or its cocoa wrapper NSXMLParser, the only xml libraries natively included on the iPhone.

Parsing the response

The iPhone does not have the NSXML-family of classes that OSX Cocoa provides. We have to use libxml2 instead. Libxml’s xmlReader module is a good place to start. Using the data received from our last chunk above, we can create a reader object:

reader = xmlReaderForMemory([data bytes], [data length], 
                                [[endpoint absoluteString] UTF8String], 
                                nil,  XML_PARSE_NOBLANKS | 
                                      XML_PARSE_NOCDATA  | 
                                      XML_PARSE_NOERROR  | 
                                      XML_PARSE_NOWARNING);

We can then iterate through each node of the xml response, scanning for the pieces that interest us. These pieces can then be stored in our own objects, or an NSArray or NSDictionary:

while (xmlTextReaderRead(reader) != 1) {
  //...find nodes that are important
  //...grab the values we want out of these nodes
}

See the api documentation for the xmlReader module, consult an xmlReader Cocoa tutorial that includes examples of the parser in usage, or take a look at the iPhone sample code in Omniture’s Developer Portal.

Comments (0)

Must be logged in to comment. or register now to comment!