313 lines
10 KiB
Objective-C
313 lines
10 KiB
Objective-C
//
|
|
// 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
|