New file |
| | |
| | | // |
| | | // ASIS3Request.m |
| | | // Part of ASIHTTPRequest -> http://allseeing-i.com/ASIHTTPRequest |
| | | // |
| | | // Created by Ben Copsey on 30/06/2009. |
| | | // Copyright 2009 All-Seeing Interactive. All rights reserved. |
| | | // |
| | | |
| | | #import "ASIS3Request.h" |
| | | #import <CommonCrypto/CommonHMAC.h> |
| | | |
| | | NSString *const ASIS3AccessPolicyPrivate = @"private"; |
| | | NSString *const ASIS3AccessPolicyPublicRead = @"public-read"; |
| | | NSString *const ASIS3AccessPolicyPublicReadWrite = @"public-read-write"; |
| | | NSString *const ASIS3AccessPolicyAuthenticatedRead = @"authenticated-read"; |
| | | NSString *const ASIS3AccessPolicyBucketOwnerRead = @"bucket-owner-read"; |
| | | NSString *const ASIS3AccessPolicyBucketOwnerFullControl = @"bucket-owner-full-control"; |
| | | |
| | | NSString *const ASIS3RequestSchemeHTTP = @"http"; |
| | | NSString *const ASIS3RequestSchemeHTTPS = @"https"; |
| | | |
| | | static NSString *sharedAccessKey = nil; |
| | | static NSString *sharedSecretAccessKey = nil; |
| | | |
| | | // Private stuff |
| | | @interface ASIS3Request () |
| | | + (NSData *)HMACSHA1withKey:(NSString *)key forString:(NSString *)string; |
| | | @end |
| | | |
| | | @implementation ASIS3Request |
| | | |
| | | - (id)initWithURL:(NSURL *)newURL |
| | | { |
| | | self = [super initWithURL:newURL]; |
| | | // After a bit of experimentation/guesswork, this number seems to reduce the chance of a 'RequestTimeout' error |
| | | [self setPersistentConnectionTimeoutSeconds:20]; |
| | | [self setRequestScheme:ASIS3RequestSchemeHTTP]; |
| | | return self; |
| | | } |
| | | |
| | | |
| | | - (void)dealloc |
| | | { |
| | | [currentXMLElementContent release]; |
| | | [currentXMLElementStack release]; |
| | | [dateString release]; |
| | | [accessKey release]; |
| | | [secretAccessKey release]; |
| | | [accessPolicy release]; |
| | | [requestScheme release]; |
| | | [super dealloc]; |
| | | } |
| | | |
| | | |
| | | - (void)setDate:(NSDate *)date |
| | | { |
| | | [self setDateString:[[ASIS3Request S3RequestDateFormatter] stringFromDate:date]]; |
| | | } |
| | | |
| | | - (ASIHTTPRequest *)HEADRequest |
| | | { |
| | | ASIS3Request *headRequest = (ASIS3Request *)[super HEADRequest]; |
| | | [headRequest setAccessKey:[self accessKey]]; |
| | | [headRequest setSecretAccessKey:[self secretAccessKey]]; |
| | | return headRequest; |
| | | } |
| | | |
| | | - (NSMutableDictionary *)S3Headers |
| | | { |
| | | NSMutableDictionary *headers = [NSMutableDictionary dictionary]; |
| | | if ([self accessPolicy]) { |
| | | [headers setObject:[self accessPolicy] forKey:@"x-amz-acl"]; |
| | | } |
| | | return headers; |
| | | } |
| | | |
| | | - (void)main |
| | | { |
| | | if (![self url]) { |
| | | [self buildURL]; |
| | | } |
| | | [super main]; |
| | | } |
| | | |
| | | - (NSString *)canonicalizedResource |
| | | { |
| | | return @"/"; |
| | | } |
| | | |
| | | - (NSString *)stringToSignForHeaders:(NSString *)canonicalizedAmzHeaders resource:(NSString *)canonicalizedResource |
| | | { |
| | | return [NSString stringWithFormat:@"%@\n\n\n%@\n%@%@",[self requestMethod],[self dateString],canonicalizedAmzHeaders,canonicalizedResource]; |
| | | } |
| | | |
| | | - (void)buildRequestHeaders |
| | | { |
| | | if (![self url]) { |
| | | [self buildURL]; |
| | | } |
| | | [super buildRequestHeaders]; |
| | | |
| | | // If an access key / secret access key haven't been set for this request, let's use the shared keys |
| | | if (![self accessKey]) { |
| | | [self setAccessKey:[ASIS3Request sharedAccessKey]]; |
| | | } |
| | | if (![self secretAccessKey]) { |
| | | [self setSecretAccessKey:[ASIS3Request sharedSecretAccessKey]]; |
| | | } |
| | | // If a date string hasn't been set, we'll create one from the current time |
| | | if (![self dateString]) { |
| | | [self setDate:[NSDate date]]; |
| | | } |
| | | [self addRequestHeader:@"Date" value:[self dateString]]; |
| | | |
| | | // Ensure our formatted string doesn't use '(null)' for the empty path |
| | | NSString *canonicalizedResource = [self canonicalizedResource]; |
| | | |
| | | // Add a header for the access policy if one was set, otherwise we won't add one (and S3 will default to private) |
| | | NSMutableDictionary *amzHeaders = [self S3Headers]; |
| | | NSString *canonicalizedAmzHeaders = @""; |
| | | for (NSString *header in [amzHeaders keysSortedByValueUsingSelector:@selector(compare:)]) { |
| | | canonicalizedAmzHeaders = [NSString stringWithFormat:@"%@%@:%@\n",canonicalizedAmzHeaders,[header lowercaseString],[amzHeaders objectForKey:header]]; |
| | | [self addRequestHeader:header value:[amzHeaders objectForKey:header]]; |
| | | } |
| | | |
| | | // Jump through hoops while eating hot food |
| | | NSString *stringToSign = [self stringToSignForHeaders:canonicalizedAmzHeaders resource:canonicalizedResource]; |
| | | NSString *signature = [ASIHTTPRequest base64forData:[ASIS3Request HMACSHA1withKey:[self secretAccessKey] forString:stringToSign]]; |
| | | NSString *authorizationString = [NSString stringWithFormat:@"AWS %@:%@",[self accessKey],signature]; |
| | | [self addRequestHeader:@"Authorization" value:authorizationString]; |
| | | |
| | | |
| | | } |
| | | |
| | | - (void)requestFinished |
| | | { |
| | | if ([[[self responseHeaders] objectForKey:@"Content-Type"] isEqualToString:@"application/xml"]) { |
| | | [self parseResponseXML]; |
| | | } |
| | | if (![self error]) { |
| | | [super requestFinished]; |
| | | } |
| | | } |
| | | |
| | | #pragma mark Error XML parsing |
| | | |
| | | - (void)parseResponseXML |
| | | { |
| | | NSData* xmlData = [self responseData]; |
| | | if (![xmlData length]) { |
| | | return; |
| | | } |
| | | NSXMLParser *parser = [[[NSXMLParser alloc] initWithData:xmlData] autorelease]; |
| | | [self setCurrentXMLElementStack:[NSMutableArray array]]; |
| | | [parser setDelegate:self]; |
| | | [parser setShouldProcessNamespaces:NO]; |
| | | [parser setShouldReportNamespacePrefixes:NO]; |
| | | [parser setShouldResolveExternalEntities:NO]; |
| | | [parser parse]; |
| | | |
| | | } |
| | | |
| | | - (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError |
| | | { |
| | | [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIS3ResponseParsingFailedType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Parsing the resposnse failed",NSLocalizedDescriptionKey,parseError,NSUnderlyingErrorKey,nil]]]; |
| | | } |
| | | |
| | | - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict |
| | | { |
| | | [self setCurrentXMLElementContent:@""]; |
| | | [[self currentXMLElementStack] addObject:elementName]; |
| | | } |
| | | |
| | | - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName |
| | | { |
| | | [[self currentXMLElementStack] removeLastObject]; |
| | | if ([elementName isEqualToString:@"Message"]) { |
| | | [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIS3ResponseErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[self currentXMLElementContent],NSLocalizedDescriptionKey,nil]]]; |
| | | // Handle S3 connection expiry errors |
| | | } else if ([elementName isEqualToString:@"Code"]) { |
| | | if ([[self currentXMLElementContent] isEqualToString:@"RequestTimeout"]) { |
| | | if ([self retryUsingNewConnection]) { |
| | | [parser abortParsing]; |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string |
| | | { |
| | | [self setCurrentXMLElementContent:[[self currentXMLElementContent] stringByAppendingString:string]]; |
| | | } |
| | | |
| | | - (id)copyWithZone:(NSZone *)zone |
| | | { |
| | | ASIS3Request *newRequest = [super copyWithZone:zone]; |
| | | [newRequest setAccessKey:[self accessKey]]; |
| | | [newRequest setSecretAccessKey:[self secretAccessKey]]; |
| | | [newRequest setRequestScheme:[self requestScheme]]; |
| | | [newRequest setAccessPolicy:[self accessPolicy]]; |
| | | return newRequest; |
| | | } |
| | | |
| | | |
| | | #pragma mark Shared access keys |
| | | |
| | | + (NSString *)sharedAccessKey |
| | | { |
| | | return sharedAccessKey; |
| | | } |
| | | |
| | | + (void)setSharedAccessKey:(NSString *)newAccessKey |
| | | { |
| | | [sharedAccessKey release]; |
| | | sharedAccessKey = [newAccessKey retain]; |
| | | } |
| | | |
| | | + (NSString *)sharedSecretAccessKey |
| | | { |
| | | return sharedSecretAccessKey; |
| | | } |
| | | |
| | | + (void)setSharedSecretAccessKey:(NSString *)newAccessKey |
| | | { |
| | | [sharedSecretAccessKey release]; |
| | | sharedSecretAccessKey = [newAccessKey retain]; |
| | | } |
| | | |
| | | |
| | | #pragma mark helpers |
| | | |
| | | + (NSString *)stringByURLEncodingForS3Path:(NSString *)key |
| | | { |
| | | if (!key) { |
| | | return @"/"; |
| | | } |
| | | NSString *path = [(NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)key, NULL, CFSTR(":?#[]@!$ &'()*+,;=\"<>%{}|\\^~`"), CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)) autorelease]; |
| | | if (![[path substringWithRange:NSMakeRange(0, 1)] isEqualToString:@"/"]) { |
| | | path = [@"/" stringByAppendingString:path]; |
| | | } |
| | | return path; |
| | | } |
| | | |
| | | // Thanks to Tom Andersen for pointing out the threading issues and providing this code! |
| | | + (NSDateFormatter*)S3ResponseDateFormatter |
| | | { |
| | | // We store our date formatter in the calling thread's dictionary |
| | | // NSDateFormatter is not thread-safe, this approach ensures each formatter is only used on a single thread |
| | | // This formatter can be reused 1000 times in parsing a single response, so it would be expensive to keep creating new date formatters |
| | | NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary]; |
| | | NSDateFormatter *dateFormatter = [threadDict objectForKey:@"ASIS3ResponseDateFormatter"]; |
| | | if (dateFormatter == nil) { |
| | | dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; |
| | | [dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]]; |
| | | [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; |
| | | [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'.000Z'"]; |
| | | [threadDict setObject:dateFormatter forKey:@"ASIS3ResponseDateFormatter"]; |
| | | } |
| | | return dateFormatter; |
| | | } |
| | | |
| | | + (NSDateFormatter*)S3RequestDateFormatter |
| | | { |
| | | NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary]; |
| | | NSDateFormatter *dateFormatter = [threadDict objectForKey:@"ASIS3RequestHeaderDateFormatter"]; |
| | | if (dateFormatter == nil) { |
| | | dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; |
| | | // Prevent problems with dates generated by other locales (tip from: http://rel.me/t/date/) |
| | | [dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]]; |
| | | [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; |
| | | [dateFormatter setDateFormat:@"EEE, d MMM yyyy HH:mm:ss Z"]; |
| | | [threadDict setObject:dateFormatter forKey:@"ASIS3RequestHeaderDateFormatter"]; |
| | | } |
| | | return dateFormatter; |
| | | |
| | | } |
| | | |
| | | // From: http://stackoverflow.com/questions/476455/is-there-a-library-for-iphone-to-work-with-hmac-sha-1-encoding |
| | | |
| | | + (NSData *)HMACSHA1withKey:(NSString *)key forString:(NSString *)string |
| | | { |
| | | NSData *clearTextData = [string dataUsingEncoding:NSUTF8StringEncoding]; |
| | | NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; |
| | | |
| | | uint8_t digest[CC_SHA1_DIGEST_LENGTH] = {0}; |
| | | |
| | | CCHmacContext hmacContext; |
| | | CCHmacInit(&hmacContext, kCCHmacAlgSHA1, keyData.bytes, keyData.length); |
| | | CCHmacUpdate(&hmacContext, clearTextData.bytes, clearTextData.length); |
| | | CCHmacFinal(&hmacContext, digest); |
| | | |
| | | return [NSData dataWithBytes:digest length:CC_SHA1_DIGEST_LENGTH]; |
| | | } |
| | | |
| | | + (NSString *)S3Host |
| | | { |
| | | return @"s3.amazonaws.com"; |
| | | } |
| | | |
| | | - (void)buildURL |
| | | { |
| | | } |
| | | |
| | | @synthesize dateString; |
| | | @synthesize accessKey; |
| | | @synthesize secretAccessKey; |
| | | @synthesize currentXMLElementContent; |
| | | @synthesize currentXMLElementStack; |
| | | @synthesize accessPolicy; |
| | | @synthesize requestScheme; |
| | | @end |