Using Drupal Services in an iPhone App with an API Key


I must have spent two whole days on this. You'd think it would be easier to find the right way to do this, but somehow the answer was really hard to find and in the end what works is a combination of different techniques I found while scouring the interweb.
I must have spent two whole days on this. You'd think it would be easier to find the right way to do this, but somehow the answer was really hard to find and in the end what works is a combination of different techniques I found while scouring the interweb.
 
Forgive me if this approach doesn't work for you.
 
In this example I am simply trying to contact the "node.get"method which is available via the Node Service module.  I decided to use JSON-RPC because I'm used to working with XML-RPC for AS3 and I figured I should learn something new.
 
The current version of Drupal at the time of writing this article, is version 6.16.
 
Here's a list of the Drupal modules that I used:
  • Services
  • Node Service (shipped with the Services module)
  • JSON-RPC Server 

  •  
    After installing and enabling the modules, I went over to Permissions and (for testing purposes) gave anonymous users access to "load node data"under node_service module.
     
    For the Services configuration, under Settings I have the Authentication module set to 'Key Authentication ', which happens to be the only option.  I've got 'Use keys ' checked, the 'Token expiry time ' set to 60 (just to make sure there are no problems between the timestamp from the device and the timestamp from my server) and I have 'Use sessid ' checked for good measure.
     
    I've created a new key for my application and assigned "*"as the domain.  I've checked both 'node.get ' and 'system.connect ' under 'Method access '.
    That's it for Drupal, now on to iPhone...
     
    I used several libraries.  Not sure if they're all necessary, if you have a better solution, please let me know.  First and foremost to facilitate the connection.
     
  • ASIHTTP
  • JSON framework
  • Common Crypto

  •  
    I've just dragged the JSON folder into a Libraries folder I have setup for all my shared libraries.  The Common Crypto library ships with the iPhone SDK.
    view source
    print
    ? 1 #import "ASIFormDataRequest.h"#import "ASINetworkQueue.h"#import "JSON.h"#import<CommonCrypto/CommonHMAC.h>
     
    I'm using iPhone SDK 3.1. 
     
    I have a two phase approach. Again, may not be the best solution so let me know if you have a better idea.
     
    Phase 1  contacts the server using the system.connect  method and looks for the sessid that is returned.
    view source
    print
    ? 1 ASIHTTPRequest *request = [[[ASIFormDataRequest alloc] initWithURL:[ NSURL URLWithString:@ "http://path-to-your-server/services/json-rpc " ]] autorelease];[request addRequestHeader:@ "Content-Type" value:@ "application/json; charset=utf-8" ];[request setRequestMethod:@ "POST" ];[request setTimeOutSeconds:20];[request setDelegate: self ];[request setDidFinishSelector: @selector (gotResponse:)];[request setDidFailSelector: @selector (requestFailed:)]; NSString *jsonString = @ "{ \"version\": \"1.1\", \"method\": \"system.connect\", \"id\": \"2\", \"params\": []}" ;[request appendPostData: [jsonString dataUsingEncoding: NSUTF8StringEncoding ]]; [request startSynchronous];
     
    I'm rolling my own JSON-RPC here, because I couldn't find anything for cocoa that does JSON-RPC.  Eventually I will let the JSON library create the JSON formatting and write my own class that packs it up using version 1.1 of the JSON-RPC standard .
     
    Upon receiving the response from the server, the gotResponse  method is called.
    view source
    print
    ? 1 - ( void )gotResponse:(ASIHTTPRequest *)requestResponse{    NSLog (@ "got response" );    // using the json-framework, convert the response string into a dictionary    NSDictionary *dictionary = [self objectWithString:[requestResponse responseString]];    // pass the sessid from the result    [self getNodeUsingAPIKey:[[dictionary objectForKey:@"result"] objectForKey:@"sessid"]];}- (id)objectWithString:(NSString *)jsonString{    SBJSON *jsonParser = [SBJSON new];    // Parse the JSON into an Object    return [jsonParser objectWithString:jsonString error:NULL];}
     
    Here I'm using a simple method called objectWithString  to convert the response from the server which is a JSON formatted string to an NSDictionary so that each property can be accessed using the property name as the key.
     
    Then I call a method called getNodeUsingAPIKey  and pass it the sessid value from the response dictionary.
     
    Here's where things start to get interesting...
    view source
    print
    ? 1 - ( void )getNodeUsingAPIKey:( NSString *)sessid{    NSLog (@ "get node using api key" );    ASIHTTPRequest *request = [[[ASIFormDataRequest alloc] initWithURL:[ NSURL URLWithString:@ "http://path-to-your-server/services/json-rpc " ]] autorelease];    [request addRequestHeader:@ "Content-Type" value:@ "application/json; charset=utf-8" ];    [request setRequestMethod:@ "POST" ];    [request setTimeOutSeconds:20];    [request setDelegate: self ];    [request setDidFinishSelector: @selector (gotResponse:)];    [request setDidFailSelector: @selector (requestFailed:)];    NSString *timestamp = [ NSString stringWithFormat:@ "%d" , ( long )[[ NSDate date] timeIntervalSince1970]];    NSString *nonce = [ NSString stringWithFormat:@ "%d" ,arc4random()];    NSLog (@ "timestamp: %@
    nonce: %@"
    ,timestamp, nonce );    NSString *hashParams = [ NSString stringWithFormat:@ "%@;*;%@;node.get" ,timestamp,nonce];    NSString *hash = [ self hashString:hashParams usingAPIKey:@ "c4c6e43dbf9f55asrerfa12312789" ];    NSString *jsonString = [ NSString stringWithFormat:@ "{ \"version\": \"1.1\", \"method\": \"node.get\", \"id\": \"11\", \"params\": [\"%@\", \"*\", \"%@\", \"%@\",\"%@\",1]}" ,hash,timestamp,nonce,sessid];    [request appendPostData: [jsonString dataUsingEncoding: NSUTF8StringEncoding ]];    [request startSynchronous];}
     
    Essentially, I'm creating a second request (phase 2 ) and using the sessid and with the required timestamp, domain, nonce and api key, generating the hash.
     
    This is the method that makes everything work...
    view source
    print
    ? 1 - (NSString *)hashString:(NSString *)inputString usingAPIKey:(NSString *)theAPIKey{    NSData *secretData = [theAPIKey dataUsingEncoding:NSUTF8StringEncoding];    NSData *clearTextData = [inputString dataUsingEncoding:NSUTF8StringEncoding];    uint8_t digest[CC_SHA256_DIGEST_LENGTH] = { 0 };    CCHmacContext hmacContext;    CCHmacInit(&hmacContext, kCCHmacAlgSHA256, secretData.bytes, secretData.length);    CCHmacUpdate(&hmacContext, clearTextData.bytes, clearTextData.length);    CCHmacFinal(&hmacContext, digest);    NSData *hashedData = [NSData dataWithBytes:digest length: 32 ];    NSString *hashedString = [self stringWithHexBytes:hashedData];    NSLog(@ "hash string: %@ length: %d" ,[hashedString lowercaseString],[hashedString length]);    return [hashedString lowercaseString];}
    This method took me two days to figure out.  I ended up using this online sha256 generator to create a temporary hash that I can use to compare results while I worked out the method.
     
    Finally after searching high and low, google led me here .  First I set my secretData  as UTF8 version of my api key.  Then I set my clearTextData  as the UTF8 version of timestamp;domain;nonce;method .  The domain is the same domain I used when I setup my key in Drupal.
     
    The hashString method basically uses sha256 to hash the api key then combines it with the clearTextData using Hmac to generate the hashedData .  Using the "stringWithHexBytes "method I pass the hashedData  and it returns a string.  For whatever reason the string is uppercase and Drupal wants to see it as lowcase, so when I return the hashedString  I convert it to lowercase.
     
    Now that all of my ducks are in a row, I can freely access Drupal services using an API Key from with my iPhone apps.
     
    Click here to download the sample Xcode project.
     
    Other Links:
    stackoverflow.com
    Very Nice Industries iPhone Apps