// // OpenStackRequest.m // OpenStack // // Created by Mike Mayo on 10/8/10. // The OpenStack project is provided under the Apache 2.0 license. // #import "OpenStackRequest.h" #import "Provider.h" #import "OpenStackAccount.h" #import "JSON.h" #import "Container.h" #import "StorageObject.h" #import "Folder.h" #import "AccountManager.h" #import "APICallback.h" #import "APILogEntry.h" #import "NSString+Conveniences.h" static NSRecursiveLock *accessDetailsLock = nil; @implementation OpenStackRequest @synthesize account, callback, retriedCount, errorAlerter; - (BOOL)isSuccess { return (200 <= [self responseStatusCode]) && ([self responseStatusCode] <= 299); } - (void)notify:(NSString *)name { NSDictionary *callbackUserInfo = [NSDictionary dictionaryWithObject:self forKey:@"response"]; NSNotification *notification = [NSNotification notificationWithName:name object:nil userInfo:callbackUserInfo]; [[NSNotificationCenter defaultCenter] postNotification:notification]; } - (void)notify { NSString *observeName = [NSString stringWithFormat:@"%@ %@ %@", [self isSuccess] ? @"SUCCESS" : @"FAILURE", self.requestMethod, [self.url description]]; NSString *callbackName = [NSString stringWithFormat:@"%@ %@ %@ %@", [self isSuccess] ? @"SUCCESS" : @"FAILURE", self.requestMethod, [self.url description], self.callback.uuid]; NSDictionary *callbackUserInfo = [NSDictionary dictionaryWithObject:self forKey:@"response"]; NSNotification *observeNotification = [NSNotification notificationWithName:observeName object:nil userInfo:callbackUserInfo]; [[NSNotificationCenter defaultCenter] postNotification:observeNotification]; NSNotification *callbackNotification = [NSNotification notificationWithName:callbackName object:nil userInfo:callbackUserInfo]; [[NSNotificationCenter defaultCenter] postNotification:callbackNotification]; } - (NSString *)responseString { if (retried) { return [retriedRequest responseString]; } else { return [super responseString]; } } - (NSData *)responseData { if (retried) { return [retriedRequest responseData]; } else { return [super responseData]; } } - (NSDictionary *)responseHeaders { if (retried) { return [retriedRequest responseHeaders]; } else { return [super responseHeaders]; } } - (int)responseStatusCode { if (retried) { return [retriedRequest responseStatusCode]; } else { return [super responseStatusCode]; } } - (NSString *)responseStatusMessage { if (retried) { return [retriedRequest responseStatusMessage]; } else { return [super responseStatusMessage]; } } - (void)setCompletionBlock:(ASIBasicBlock)aCompletionBlock { [super setCompletionBlock:aCompletionBlock]; [backupCompletionBlock release]; backupCompletionBlock = [aCompletionBlock copy]; } - (void)setFailedBlock:(ASIBasicBlock)aFailedBlock { [super setFailedBlock:aFailedBlock]; [backupFailureBlock release]; backupFailureBlock = [aFailedBlock copy]; } #pragma mark - #pragma mark Generic Constructors + (void)initialize { if (self == [OpenStackRequest class]) { accessDetailsLock = [[NSRecursiveLock alloc] init]; } } + (id)request:(OpenStackAccount *)account method:(NSString *)method url:(NSURL *)url { OpenStackRequest *request = [[[OpenStackRequest alloc] initWithURL:url] autorelease]; request.account = account; [request setRequestMethod:method]; [request addRequestHeader:@"X-Auth-Token" value:[account authToken]]; [request addRequestHeader:@"Content-Type" value:@"application/json"]; [request setTimeOutSeconds:60]; [request setNumberOfTimesToRetryOnTimeout:5]; request.retriedCount = 0; return request; } + (id)getSharingAccountsRequest:(OpenStackAccount *)account { NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@?format=json", account.provider.authEndpointURL]]; return [OpenStackRequest request:account method:@"GET" url:url]; } + (id)filesRequest:(OpenStackAccount *)account method:(NSString *)method path:(NSString *)path { NSString *urlString = [account.filesURL description]; if (account.sharingAccount) { NSRange authUserRange = [urlString rangeOfString:account.username]; urlString = [NSString stringWithFormat:@"%@%@", [urlString substringToIndex:authUserRange.location], account.sharingAccount]; } NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@?format=json%@", urlString, path, account.shared ? @"&shared=" : @""]]; return [OpenStackRequest request:account method:method url:url]; } #pragma mark - #pragma mark Auth Retry - (void)authRetrySucceded:(OpenStackRequest *)retryRequest { self.account.authToken = [[retryRequest responseHeaders] objectForKey:@"X-Auth-Token"]; [self.account persist]; // try the original request again! retried = YES; retriedRequest = [self copy]; [retriedRequest addRequestHeader:@"X-Auth-Token" value:self.account.authToken]; if (backupCompletionBlock) { [retriedRequest setCompletionBlock:^{ backupCompletionBlock(); }]; } if (backupFailureBlock) { [retriedRequest setFailedBlock:^{ backupFailureBlock(); }]; } [retriedRequest startSynchronous]; } - (void)authRetryFailed:(OpenStackRequest *)retryRequest { // if it fails due to bad connection, try again? NSNotification *notification = [NSNotification notificationWithName:[self.account.manager notificationName:@"authRetryFailed" identifier:0] object:nil userInfo:[NSDictionary dictionaryWithObject:retryRequest forKey:@"request"]]; [[NSNotificationCenter defaultCenter] postNotification:notification]; } #pragma mark - #pragma mark ASIHTTPRequest Overrides - (void)failWithError:(NSError *)theError { if (responseStatusCode == 401 && ![url isEqual:account.provider.authEndpointURL]) { // auth is expired, so get a fresh token if (account && ![account.provider isGRNet]) { OpenStackRequest *retryRequest = [OpenStackRequest authenticationRequest:account]; retryRequest.delegate = self; retryRequest.didFinishSelector = @selector(authRetrySucceded:); retryRequest.didFailSelector = @selector(authRetryFailed:); [retryRequest startSynchronous]; } } else if (responseStatusCode == 503) { NSNotification *notification = [NSNotification notificationWithName:@"serviceUnavailable" object:nil userInfo:nil]; [[NSNotificationCenter defaultCenter] postNotification:notification]; // [super failWithError:theError]; } else if (responseStatusCode == 0) { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; if (![defaults boolForKey:@"already_failed_on_connection"]) { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Connection Error" message:@"Please check your connection or API URL and try again." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; [alert release]; } [defaults setBool:YES forKey:@"already_failed_on_connection"]; [defaults synchronize]; } [super failWithError:theError]; } #pragma mark - Authentication + (OpenStackRequest *)authenticationRequest:(OpenStackAccount *)account { OpenStackRequest *request = [[[OpenStackRequest alloc] initWithURL:account.provider.authEndpointURL] autorelease]; request.account = account; [request addRequestHeader:@"X-Auth-User" value:account.username]; if (account.authToken) { [request addRequestHeader:@"X-Auth-Token" value:account.authToken]; } else { [request addRequestHeader:@"X-Auth-Token" value:@""]; } return request; } #pragma mark - #pragma mark Object Storage Requests + (OpenStackRequest *)getStorageAccountInfoRequest:(OpenStackAccount *)account { return [OpenStackRequest filesRequest:account method:@"HEAD" path:@""]; } + (OpenStackRequest *)getContainersRequest:(OpenStackAccount *)account { return [OpenStackRequest filesRequest:account method:@"GET" path:@""]; } - (NSMutableDictionary *)containers { SBJSON *parser = [[SBJSON alloc] init]; NSArray *jsonObjects = [parser objectWithString:[self responseString]]; NSMutableDictionary *objects = [NSMutableDictionary dictionaryWithCapacity:[jsonObjects count]]; for (int i = 0; i < [jsonObjects count]; i++) { NSDictionary *dict = [jsonObjects objectAtIndex:i]; Container *container = [Container fromJSON:dict]; [objects setObject:container forKey:container.name]; } [parser release]; return objects; } + (OpenStackRequest *)createContainerRequest:(OpenStackAccount *)account container:(Container *)container { return [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]]; } + (OpenStackRequest *)deleteContainerRequest:(OpenStackAccount *)account container:(Container *)container { return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]]; } + (OpenStackRequest *)getObjectsRequest:(OpenStackAccount *)account container:(Container *)container { return [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]]; } - (NSMutableDictionary *)objects { SBJSON *parser = [[SBJSON alloc] init]; NSArray *jsonObjects = [parser objectWithString:[self responseString]]; NSMutableDictionary *objects = [[[NSMutableDictionary alloc] initWithCapacity:[jsonObjects count]] autorelease]; for (int i = 0; i < [jsonObjects count]; i++) { NSDictionary *dict = [jsonObjects objectAtIndex:i]; StorageObject *object = [StorageObject fromJSON:dict]; [objects setObject:object forKey:object.name]; } [parser release]; return objects; } + (OpenStackRequest *)getContainerInfoRequest:(OpenStackAccount *)account container:(Container *)container { return [OpenStackRequest filesRequest:account method:@"HEAD" path:[NSString stringWithFormat:@"/%@",[NSString encodeToPercentEscape:container.name]]]; } + (OpenStackRequest *)getObjectInfoRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object { NSString *objectFullPath = object.fullPath; if ([objectFullPath hasPrefix:@"/"]) objectFullPath = [objectFullPath substringFromIndex:1]; return [OpenStackRequest filesRequest:account method:@"HEAD" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:objectFullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]]; } + (OpenStackRequest *)getObjectInfoRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object version:(NSString *)version { OpenStackRequest *request = [OpenStackRequest getObjectInfoRequest:account container:container object:object]; request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=%@",request.url.description, version]]; return request; } + (OpenStackRequest *)getObjectVersionsRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object { OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]]; request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=list",request.url.description]]; return request; } + (OpenStackRequest *)getObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object { return [OpenStackRequest filesRequest:account method:@"GET" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]]; } + (OpenStackRequest *)getObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object version:(NSString *)version { OpenStackRequest *request = [OpenStackRequest getObjectRequest:account container:container object:object]; if (version) { request.url = [NSURL URLWithString:[NSString stringWithFormat:@"%@&version=%@",request.url.description, version]]; } return request; } + (OpenStackRequest *)writeObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object { NSString *fullPath = object.fullPath; if ([fullPath characterAtIndex:0] == '/') { fullPath = [fullPath substringFromIndex:1]; } OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]]; if (object.sharing) [request.requestHeaders setObject:object.sharing forKey:@"X-Object-Sharing"]; NSString *metadataKeyHeaderPrefix; if ([fullPath length] == 0) metadataKeyHeaderPrefix = @"X-Container-Meta-"; else metadataKeyHeaderPrefix = @"X-Object-Meta-"; for (NSString *metadataKey in object.metadata) { NSString *metadataKeyHeader = [NSString stringWithFormat:@"%@%@", metadataKeyHeaderPrefix, metadataKey]; metadataKeyHeader = [NSString encodeToPercentEscape:metadataKeyHeader]; NSString *metadataValue = [NSString encodeToPercentEscape:[object.metadata objectForKey:metadataKey]]; [request.requestHeaders setObject:metadataValue forKey:metadataKeyHeader]; } [request setPostBody:[NSMutableData dataWithData:object.data]]; [request.requestHeaders setObject:object.contentType forKey:@"Content-Type"]; return request; } + (OpenStackRequest *)writeObjectMetadataRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object { NSString *fullPath = object.fullPath; if ([fullPath length] != 0 && [fullPath characterAtIndex:0] == '/') { fullPath = [fullPath substringFromIndex:1]; } NSString *metadataKeyHeaderPrefix; if ([fullPath length] == 0) metadataKeyHeaderPrefix = @"X-Container-Meta-"; else metadataKeyHeaderPrefix = @"X-Object-Meta-"; OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"POST" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]]; for (NSString *metadataKey in object.metadata) { NSString *metadataKeyHeader = [NSString stringWithFormat:@"%@%@", metadataKeyHeaderPrefix, metadataKey]; metadataKeyHeader = [NSString encodeToPercentEscape:metadataKeyHeader]; NSString *metadataValue = [NSString encodeToPercentEscape:[object.metadata objectForKey:metadataKey]]; [request.requestHeaders setObject:metadataValue forKey:metadataKeyHeader]; } if (!account.sharingAccount) { NSString *objectIsPublic = ([object.publicURI length] > 0) ? @"true" : @"false"; [request.requestHeaders setObject:objectIsPublic forKey:@"X-Object-Public"]; if (object.sharing) { NSString *urlEncodedSharingString = [NSString encodeToPercentEscape:object.sharing]; [request.requestHeaders setObject:urlEncodedSharingString forKey:@"X-Object-Sharing"]; } } return request; } + (OpenStackRequest *)deleteObjectRequest:(OpenStackAccount *)account container:(Container *)container object:(StorageObject *)object { if ([object.fullPath characterAtIndex:0] == '/') { return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]]; } else { return [OpenStackRequest filesRequest:account method:@"DELETE" path:[NSString stringWithFormat:@"/%@/%@",[NSString encodeToPercentEscape:container.name], [NSString encodeToPercentEscape:object.fullPath charactersToEncode:@"!*'();:@&=+$,?%#[]"]]]; } } + (OpenStackRequest *)writeContainerPolicyRequest:(OpenStackAccount *)account container:(Container *)container { OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"PUT" path:[NSString stringWithFormat:@"/%@", [NSString encodeToPercentEscape:container.name]]]; [request.requestHeaders setObject:container.versioning forKey:@"X-Container-Policy-Versioning"]; [request.requestHeaders setObject:[NSString stringWithFormat:@"%u", container.quota] forKey:@"X-Container-Policy-Quota"]; return request; } + (OpenStackRequest *)writeAccountMetadataRequest:(OpenStackAccount *)account withAccountInfo:(NSDictionary *)accountInfo { OpenStackRequest *request = [OpenStackRequest filesRequest:account method:@"POST" path:@""]; NSMutableDictionary *groups = [accountInfo objectForKey:@"groups"]; for (NSString *groupName in groups) { NSString *group = [NSString encodeToPercentEscape:[groups objectForKey:groupName]]; groupName = [NSString encodeToPercentEscape:groupName]; [request.requestHeaders setObject:group forKey:[NSString stringWithFormat:@"X-Account-Group-%@", groupName]]; } if ([groups count] == 0) [request.requestHeaders setObject:@"" forKey:@"X-Account-Group-group"]; NSMutableDictionary *accountMetadata = [accountInfo objectForKey:@"metadata"]; for (NSString *metadataKey in accountMetadata) { NSString *metadataValue = [NSString encodeToPercentEscape:[accountMetadata objectForKey:metadataKey]]; metadataKey = [NSString encodeToPercentEscape:[accountMetadata objectForKey:metadataKey]]; [request.requestHeaders setObject:metadataValue forKey:[NSString stringWithFormat:@"X-Account-Meta-%@",metadataKey]]; } return request; } #pragma mark - #pragma mark Memory Management - (void)releaseBackupBlocksOnMainThread { NSMutableArray *blocks = [NSMutableArray array]; if (backupCompletionBlock) { [blocks addObject:backupCompletionBlock]; [backupCompletionBlock release]; backupCompletionBlock = nil; } if (backupFailureBlock) { [blocks addObject:backupFailureBlock]; [backupFailureBlock release]; backupFailureBlock = nil; } [[self class] performSelectorOnMainThread:@selector(releaseBackupBlocks:) withObject:blocks waitUntilDone:[NSThread isMainThread]]; } // Always called on main thread + (void)releaseBackupBlocks:(NSArray *)blocks { // Blocks will be released when this method exits } - (void)dealloc { [account release]; [errorAlerter release]; [self releaseBackupBlocksOnMainThread]; [super dealloc]; } @end