/* * cocos2d for iPhone: http://www.cocos2d-iphone.org * * Copyright (c) 2008-2010 Ricardo Quesada * Copyright (c) 2011 Zynga Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ #import "CLScoreServerPost.h" #import "ccMacros.h" // free function used to sort NSInteger alphabeticSort(id string1, id string2, void *reverse); NSInteger alphabeticSort(id string1, id string2, void *reverse) { if ((NSInteger *)reverse == NO) return [string2 localizedCaseInsensitiveCompare:string1]; return [string1 localizedCaseInsensitiveCompare:string2]; } @interface CLScoreServerPost (Private) -(void) addValue:(NSString*)value key:(NSString*)key; -(void) calculateHashAndAddValue:(id)value key:(NSString*)key; -(NSString*) getHashForData; -(NSData*) getBodyValues; -(NSString*) encodeData:(NSString*)data; -(NSMutableURLRequest *) scoreServerRequestWithURLString:(NSString *)url; -(BOOL) submitScore:(NSDictionary*)dict forUpdate:(BOOL)isUpdate; @end @implementation CLScoreServerPost @synthesize postStatus = postStatus_; @synthesize ranking = ranking_; @synthesize scoreDidUpdate = scoreDidUpdate_; @synthesize connection = connection_; +(id) serverWithGameName:(NSString*) name gameKey:(NSString*) key delegate:(id) delegate { return [[[self alloc] initWithGameName:name gameKey:key delegate:delegate] autorelease]; } -(id) initWithGameName:(NSString*) name gameKey:(NSString*) key delegate:(id)aDelegate { self = [super init]; if( self ) { gameKey = [key retain]; gameName = [name retain]; bodyValues = [[NSMutableArray arrayWithCapacity:5] retain]; delegate = [aDelegate retain]; receivedData = [[NSMutableData data] retain]; ranking_ = kServerPostInvalidRanking; } return self; } -(void) dealloc { CCLOGINFO(@"deallocing %@", self); [delegate release]; [gameKey release]; [gameName release]; [bodyValues release]; [receivedData release]; [connection_ release]; [super dealloc]; } #pragma mark ScoreServer send scores -(BOOL) sendScore: (NSDictionary*) dict { return [self submitScore:dict forUpdate:NO]; } -(BOOL) updateScore: (NSDictionary*) dict { if (![dict objectForKey:@"cc_playername"]) { // fail. cc_playername + cc_device_id are needed to update an score [NSException raise:@"cocosLive:updateScore" format:@"cc_playername not found"]; } return [self submitScore:dict forUpdate:YES]; } -(BOOL) submitScore: (NSDictionary*)dict forUpdate:(BOOL)isUpdate { [receivedData setLength:0]; [bodyValues removeAllObjects]; // reset status postStatus_ = kPostStatusOK; // create the request NSMutableURLRequest *post = [self scoreServerRequestWithURLString:(isUpdate ? SCORE_SERVER_UPDATE_URL : SCORE_SERVER_SEND_URL)]; CC_MD5_Init( &md5Ctx); // hash SHALL be calculated in certain order NSArray *keys = [dict allKeys]; int reverseSort = NO; NSArray *sortedKeys = [keys sortedArrayUsingFunction:alphabeticSort context:&reverseSort]; for( id key in sortedKeys ) [self calculateHashAndAddValue:[dict objectForKey:key] key:key]; // device id is hashed to prevent spoofing this same score from different devices // one way to prevent a replay attack is to send cc_id & cc_time and use it as primary keys [self addValue:[[UIDevice currentDevice] uniqueIdentifier] key:@"cc_device_id"]; [self addValue:gameName key:@"cc_gamename"]; [self addValue:[self getHashForData] key:@"cc_hash"]; [self addValue:SCORE_SERVER_PROTOCOL_VERSION key:@"cc_prot_ver"]; [post setHTTPBody: [self getBodyValues] ]; // create the connection with the request // and start loading the data self.connection=[NSURLConnection connectionWithRequest:post delegate:self]; if ( ! connection_) return NO; return YES; } -(NSMutableURLRequest *) scoreServerRequestWithURLString:(NSString *)url { NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[NSURL URLWithString: url] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0]; [request setHTTPMethod: @"POST"]; [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; return request; } -(void) calculateHashAndAddValue:(id) value key:(NSString*) key { NSString *val; // value shall be a string or nsnumber if( [value respondsToSelector:@selector(stringValue)] ) val = [value stringValue]; else if( [value isKindOfClass:[NSString class]] ) val = value; else [NSException raise:@"Invalid format for value" format:@"Invalid format for value. addValue"]; [self addValue:val key:key]; const char * data = [val UTF8String]; CC_MD5_Update( &md5Ctx, data, strlen(data) ); } -(void) addValue:(NSString*)value key:(NSString*) key { NSString *encodedValue = [self encodeData:value]; NSString *encodedKey = [self encodeData:key]; [bodyValues addObject: [NSString stringWithFormat:@"%@=%@", encodedKey, encodedValue] ]; } -(NSData*) getBodyValues { NSMutableData *data = [[NSMutableData alloc] init]; BOOL first=YES; for( NSString *s in bodyValues ) { if( !first) [data appendBytes:"&" length:1]; [data appendBytes:[s UTF8String] length:[s length]]; first = NO; } return [data autorelease]; } -(NSString*) getHashForData { NSString *ret; unsigned char pTempKey[16]; // update the hash with the secret key const char *data = [gameKey UTF8String]; CC_MD5_Update(&md5Ctx, data, strlen(data)); // then get the hash CC_MD5_Final( pTempKey, &md5Ctx); // NSData *nsdata = [NSData dataWithBytes:pTempKey length:16]; ret = [NSString stringWithString:@""]; for( int i=0;i<16;i++) { ret = [NSString stringWithFormat:@"%@%02x", ret, pTempKey[i] ]; } return ret; } -(NSString*) encodeData:(NSString*) data { NSString *newData; newData = [data stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; // '&' and '=' should be encoded manually newData = [newData stringByReplacingOccurrencesOfString:@"&" withString:@"%26"]; newData = [newData stringByReplacingOccurrencesOfString:@"=" withString:@"%3D"]; return newData; } #pragma mark NSURLConnection Delegate - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { // this method is called when the server has determined that it // has enough information to create the NSURLResponse // it can be called multiple times, for example in the case of a // redirect, so each time we reset the data. // receivedData is declared as a method instance elsewhere [receivedData setLength:0]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { // append the new data to the receivedData // receivedData is declared as a method instance elsewhere [receivedData appendData:data]; // NSString *dataString = [NSString stringWithCString:[data bytes] length: [data length]]; // CCLOG( @"data: %@", dataString); } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { CCLOG(@"Connection failed"); // wifi problems ? postStatus_ = kPostStatusConnectionFailed; // release the connection self.connection = nil; if( [delegate respondsToSelector:@selector(scorePostFail:) ] ) [delegate scorePostFail:self]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { // release the connection self.connection = nil; // NSString *dataString = [NSString stringWithCString:[receivedData bytes] length: [receivedData length]]; NSString *dataString = [NSString stringWithCString:[receivedData bytes] encoding: NSASCIIStringEncoding]; if( [dataString hasPrefix:@"OK:"] ) { // parse ranking and other possible answers NSArray *values = [dataString componentsSeparatedByString:@":"]; NSArray *answer = [ [values objectAtIndex:1] componentsSeparatedByString:@","]; NSEnumerator *nse = [answer objectEnumerator]; // Create a holder for each line we are going to work with NSString *line; // Loop through all the lines in the lines array processing each one while( (line = [nse nextObject]) ) { NSArray *keyvalue = [line componentsSeparatedByString:@"="]; // NSLog(@"%@",keyvalue); if( [[keyvalue objectAtIndex:0] isEqual:@"ranking"] ) { ranking_ = [[keyvalue objectAtIndex:1] intValue]; } else if( [[keyvalue objectAtIndex:0] isEqual:@"score_updated"] ) { scoreDidUpdate_ = [[keyvalue objectAtIndex:1] boolValue]; } } if( [delegate respondsToSelector:@selector(scorePostOk:) ] ) [delegate scorePostOk:self]; } else if( [dataString hasPrefix: @"OK"] ) { // Ok postStatus_ = kPostStatusOK; if( [delegate respondsToSelector:@selector(scorePostOk:) ] ) [delegate scorePostOk:self]; } else { NSLog(@"cocoslive: Post Score failed. Reason: %@", dataString); // Error parsing answer postStatus_ = kPostStatusPostFailed; if( [delegate respondsToSelector:@selector(scorePostFail:) ] ) [delegate scorePostFail:self]; } } -(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse { NSURLRequest *newRequest=request; if (redirectResponse) { newRequest=nil; } return newRequest; } @end