arq_restore/cocoastack/googledrive/GoogleDriveRequest.m
2014-07-28 14:20:07 -04:00

290 lines
13 KiB
Objective-C

//
// GoogleDriveRequest.m
// Arq
//
// Created by Stefan Reitshamer on 7/17/14.
// Copyright (c) 2014 Stefan Reitshamer. All rights reserved.
//
#import "GoogleDriveRequest.h"
#import "GoogleDrive.h"
#import "HTTPConnection.h"
#import "HTTPConnectionFactory.h"
#import "GoogleDriveErrorResult.h"
#import "TargetConnection.h"
#import "NSDictionary_HTTP.h"
#import "NSString+SBJSON.h"
#define INITIAL_RETRY_SLEEP (0.5)
#define RETRY_SLEEP_GROWTH_FACTOR (1.5)
#define MAX_RETRY_SLEEP (5.0)
static NSString *const kGoogleClientIDKey = @"1081461930698-ci22n9pcfkmebda0iofbt5qke01r1000.apps.googleusercontent.com";
static NSString *const kGoogleClientSecretKey = @"UGO8R1b64tw1FS1MT-3K8qJD";
@implementation GoogleDriveRequest
- (id)initWithEmailAddress:(NSString *)theEmailAddress method:(NSString *)theMethod path:(NSString *)thePath queryString:(NSString *)theQueryString refreshToken:(NSString *)theRefreshToken googleDriveDelegate:(id<GoogleDriveDelegate>)theGoogleDriveDelegate dataTransferDelegate:(id<DataTransferDelegate>)theDelegate error:(NSError **)error {
if (theQueryString != nil) {
if ([theQueryString hasPrefix:@"?"]) {
SETNSERROR([GoogleDrive errorDomain], -1, @"query string may not begin with a ?");
[self release];
return nil;
}
thePath = [[thePath stringByAppendingString:@"?"] stringByAppendingString:theQueryString];
}
if (![thePath hasPrefix:@"/upload/drive/v2"] && ![thePath hasPrefix:@"/drive/v2"]) {
SETNSERROR([GoogleDrive errorDomain], -1, @"path must begin with /upload/drive/v2 or /drive/v2");
[self release];
return nil;
}
NSString *urlString = [NSString stringWithFormat:@"https://www.googleapis.com%@", thePath];
NSURL *theURL = [[[NSURL alloc] initWithString:urlString] autorelease];
if (theURL == nil) {
SETNSERROR([GoogleDrive errorDomain], -1, @"invalid URL: %@", urlString);
[self release];
return nil;
}
return [self initWithEmailAddress:theEmailAddress method:theMethod url:theURL refreshToken:theRefreshToken googleDriveDelegate:theGoogleDriveDelegate dataTransferDelegate:theDelegate error:error];
}
- (id)initWithGetURL:(NSURL *)theURL refreshToken:(NSString *)theRefreshToken googleDriveDelegate:(id<GoogleDriveDelegate>)theGoogleDriveDelegate dataTransferDelegate:(id<DataTransferDelegate>)theDelegate error:(NSError **)error {
return [self initWithEmailAddress:nil method:@"GET" url:theURL refreshToken:theRefreshToken googleDriveDelegate:theGoogleDriveDelegate dataTransferDelegate:theDelegate error:error];
}
- (id)initWithEmailAddress:(NSString *)theEmailAddress method:(NSString *)theMethod url:(NSURL *)theURL refreshToken:(NSString *)theRefreshToken googleDriveDelegate:(id<GoogleDriveDelegate>)theGoogleDriveDelegate dataTransferDelegate:(id<DataTransferDelegate>)theDelegate error:(NSError **)error {
if (self = [super init]) {
emailAddress = [theEmailAddress retain];
method = [theMethod retain];
url = [theURL retain];
refreshToken = [theRefreshToken retain];
googleDriveDelegate = theGoogleDriveDelegate;
dataTransferDelegate = theDelegate;
extraRequestHeaders = [[NSMutableDictionary alloc] init];
responseHeaders = [[NSMutableDictionary alloc] init];
}
return self;
}
- (void)dealloc {
[emailAddress release];
[method release];
[url release];
[requestBody release];
[extraRequestHeaders release];
[responseHeaders release];
[super dealloc];
}
- (void)setRequestBody:(NSData *)theRequestBody {
[theRequestBody retain];
[requestBody release];
requestBody = theRequestBody;
}
- (void)setRequestHeader:(NSString *)value forKey:(NSString *)key {
[extraRequestHeaders setObject:value forKey:key];
}
- (int)httpResponseCode {
return httpResponseCode;
}
- (NSString *)responseHeaderForKey:(NSString *)theKey {
return [responseHeaders objectForKey:theKey];
}
- (NSData *)dataWithTargetConnectionDelegate:(id <TargetConnectionDelegate>)theDelegate error:(NSError **)error {
NSAutoreleasePool *pool = nil;
NSTimeInterval sleepTime = INITIAL_RETRY_SLEEP;
NSData *responseData = nil;
NSError *myError = nil;
for (;;) {
[pool drain];
pool = [[NSAutoreleasePool alloc] init];
BOOL needRetry = NO;
BOOL needSleep = NO;
myError = nil;
responseData = [self dataOnce:&myError];
if (responseData != nil) {
break;
}
HSLogDebug(@"GoogleDriveRequest dataOnce failed; %@", myError);
if ([myError isErrorWithDomain:[GoogleDrive errorDomain] code:ERROR_NOT_FOUND]) {
break;
}
if ([myError isTransientError]) {
needRetry = YES;
needSleep = YES;
}
BOOL refreshedToken = NO;
if ([myError isErrorWithDomain:[GoogleDrive errorDomain] code:401]) {
NSString *accessToken = [self requestAccessToken:&myError];
if (accessToken == nil) {
HSLogError(@"failed to get new access token: %@", myError);
break;
}
[googleDriveDelegate googleDriveDidChangeAccessToken:accessToken forRefreshToken:refreshToken];
needRetry = YES;
refreshedToken = YES;
}
if (!refreshedToken && (!needRetry || ![theDelegate targetConnectionShouldRetryOnTransientError:&myError])) {
HSLogError(@"%@ %@: %@", method, url, myError);
break;
}
HSLogDetail(@"retrying %@: %@", method, myError);
if (needSleep) {
[NSThread sleepForTimeInterval:sleepTime];
sleepTime *= RETRY_SLEEP_GROWTH_FACTOR;
if (sleepTime > MAX_RETRY_SLEEP) {
sleepTime = MAX_RETRY_SLEEP;
}
}
}
[responseData retain];
if (responseData == nil) {
[myError retain];
}
[pool drain];
[responseData autorelease];
if (responseData == nil) {
[myError autorelease];
SETERRORFROMMYERROR;
}
return responseData;
}
#pragma mark internal
- (NSData *)dataOnce:(NSError **)error {
id <HTTPConnection> conn = [[[HTTPConnectionFactory theFactory] newHTTPConnectionToURL:url method:method dataTransferDelegate:dataTransferDelegate] autorelease];
if (conn == nil) {
return nil;
}
[conn setRequestHostHeader];
[conn setRFC822DateRequestHeader];
if (requestBody != nil) {
[conn setRequestHeader:[NSString stringWithFormat:@"%lu", (unsigned long)[requestBody length]] forKey:@"Content-Length"];
}
for (NSString *headerKey in [extraRequestHeaders allKeys]) {
[conn setRequestHeader:[extraRequestHeaders objectForKey:headerKey] forKey:headerKey];
}
NSString *accessToken = [googleDriveDelegate googleDriveAccessTokenForRefreshToken:refreshToken];
if (accessToken == nil) {
accessToken = [self requestAccessToken:error];
if (accessToken == nil) {
return nil;
}
[googleDriveDelegate googleDriveDidChangeAccessToken:accessToken forRefreshToken:refreshToken];
}
[conn setRequestHeader:[NSString stringWithFormat:@"Bearer %@", accessToken] forKey:@"Authorization"];
bytesUploaded = 0;
HSLogDebug(@"%@ %@", method, url);
[conn setRequestHeader:[NSString stringWithFormat:@"%lu", (unsigned long)[requestBody length]] forKey:@"Content-Length"];
NSData *response = [conn executeRequestWithBody:requestBody error:error];
if (response == nil) {
return nil;
}
[responseHeaders setDictionary:[conn responseHeaders]];
httpResponseCode = [conn responseCode];
if (httpResponseCode >= 200 && httpResponseCode <= 299) {
HSLogDebug(@"HTTP %d; returning response length=%ld", httpResponseCode, (long)[response length]);
return response;
}
HSLogDebug(@"http response body: %@", [[[NSString alloc] initWithBytes:[response bytes] length:[response length] encoding:NSUTF8StringEncoding] autorelease]);
// if (httpResponseCode == HTTP_NOT_FOUND) {
// HSLogDebug(@"http response body: %@", [[[NSString alloc] initWithBytes:[response bytes] length:[response length] encoding:NSUTF8StringEncoding] autorelease]);
// S3ErrorResult *errorResult = [[[S3ErrorResult alloc] initWithAction:[NSString stringWithFormat:@"%@ %@", method, [url description]] data:response httpErrorCode:httpResponseCode] autorelease];
// NSError *myError = [errorResult error];
// NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[myError userInfo]];
// [userInfo setObject:[NSString stringWithFormat:@"%@ not found", url] forKey:NSLocalizedDescriptionKey];
// myError = [NSError errorWithDomain:[S3Service errorDomain] code:ERROR_NOT_FOUND userInfo:userInfo];
// HSLogDebug(@"%@", myError);
// SETERRORFROMMYERROR;
// return nil;
// }
// if (httpResponseCode == HTTP_METHOD_NOT_ALLOWED) {
// HSLogError(@"%@ 405 error", url);
// SETNSERROR([S3Service errorDomain], ERROR_RRS_NOT_FOUND, @"%@ 405 error", url);
// }
// if (httpResponseCode == HTTP_MOVED_TEMPORARILY) {
// NSString *location = [conn responseHeaderForKey:@"Location"];
// NSDictionary *userInfo = [NSDictionary dictionaryWithObject:location forKey:@"location"];
// NSError *myError = [NSError errorWithDomain:[S3Service errorDomain] code:ERROR_TEMPORARY_REDIRECT userInfo:userInfo];
// if (error != NULL) {
// *error = myError;
// }
// HSLogDebug(@"returning moved-temporarily error");
// return nil;
// }
GoogleDriveErrorResult *errorResult = [[[GoogleDriveErrorResult alloc] initWithAction:[NSString stringWithFormat:@"%@ %@", method, [url description]] data:response contentType:[responseHeaders objectForKey:@"Content-Type"] httpErrorCode:httpResponseCode] autorelease];
NSError *myError = [errorResult error];
HSLogDebug(@"%@ error: %@", conn, myError);
SETERRORFROMMYERROR;
return nil;
}
- (NSString *)requestAccessToken:(NSError **)error {
HSLogDebug(@"requesting access token");
id <HTTPConnection> conn = [[[HTTPConnectionFactory theFactory] newHTTPConnectionToURL:[NSURL URLWithString:@"https://accounts.google.com/o/oauth2/token"] method:@"POST" dataTransferDelegate:nil] autorelease];
if (conn == nil) {
return nil;
}
[conn setRequestHostHeader];
[conn setRFC822DateRequestHeader];
NSMutableDictionary *params = [NSMutableDictionary dictionary];
[params setObject:kGoogleClientIDKey forKey:@"client_id"];
[params setObject:kGoogleClientSecretKey forKey:@"client_secret"];
[params setObject:refreshToken forKey:@"refresh_token"];
[params setObject:@"refresh_token" forKey:@"grant_type"];
NSString *encodedParams = [params wwwFormURLEncodedString];
NSData *response = [conn executeRequestWithBody:[encodedParams dataUsingEncoding:NSUTF8StringEncoding] error:error];
if (response == nil) {
return nil;
}
if ([conn responseCode] != 200) {
if ([[conn responseHeaderForKey:@"Content-Type"] hasPrefix:@"application/json"]) {
NSString *responseJSONString = [[[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding] autorelease];
HSLogDebug(@"response JSON: %@", responseJSONString);
NSDictionary *responseJSON = [responseJSONString JSONValue:NULL];
if ([[responseJSON objectForKey:@"error"] isEqualToString:@"invalid_grant"]) {
// This Google Drive user has probably revoked our authorization.
NSString *errorDescription = @"Arq Access to this Google Drive account was revoked";
if (emailAddress != nil) {
errorDescription = [NSString stringWithFormat:@"Arq access to Google Drive account '%@' was revoked", emailAddress];
}
SETNSERROR([GoogleDrive errorDomain], ERROR_ACCESS_REVOKED, @"%@", errorDescription);
return nil;
}
}
SETNSERROR([GoogleDrive errorDomain], [conn responseCode], @"Google Drive refresh_token HTTP error %d", [conn responseCode]);
return nil;
}
NSString *responseString = [[[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding] autorelease];
NSDictionary *responseJSON = [responseString JSONValue:error];
if (responseJSON == nil) {
return nil;
}
NSString *accessToken = [responseJSON objectForKey:@"access_token"];
HSLogDebug(@"accessToken expires in %@", [responseJSON objectForKey:@"expires_in"]);
return accessToken;
}
@end