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
?
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
?
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
?
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
?
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
?
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
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:
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.
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
?
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
?
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
?
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
?
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
?
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