TNLExample/TAPI/TAPIClient.m (296 lines of code) (raw):

// // TAPIRequestManager.m // TwitterNetworkLayer // // Created on 10/17/14. // Copyright © 2020 Twitter. All rights reserved. // #import <CommonCrypto/CommonHMAC.h> #include <stdatomic.h> #import "TAPIClient.h" #import "TAPIError.h" #import "TNL_Project.h" static NSData *HMAC_SHA1(NSString *data, NSString *key); static NSData *HMAC_SHA1(NSString *data, NSString *key) { unsigned char buf[CC_SHA1_DIGEST_LENGTH]; CCHmac(kCCHmacAlgSHA1, [key UTF8String], key.length, [data UTF8String], data.length, buf); return [NSData dataWithBytes:buf length:CC_SHA1_DIGEST_LENGTH]; } @interface TAPIClient () <TNLRequestDelegate> @end @interface TAPIOperationContext : NSObject @property (nonatomic) NSOperation *loginOperation; @property (nonatomic, copy, readonly) NSString *oauthNonce; @property (nonatomic, copy) TAPIRequestCompletionBlock completionBlock; @end @interface TAPILoginOperation : TNLSafeOperation @property (nonatomic, weak) TAPIClient *client; - (BOOL)didSucceed; @end @implementation TAPIOperationContext - (instancetype)init { self = [super init]; if (self) { _oauthNonce = [[NSUUID UUID] UUIDString]; } return self; } @end @implementation TAPIClient { dispatch_queue_t _loginQueue; NSMutableArray<NSOperation *> *_loginOperations; NSString *_oauthAccessToken; NSString *_oauthAccessSecret; } + (instancetype)sharedInstance { static TAPIClient *sManager; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sManager = [[TAPIClient alloc] init]; }); return sManager; } - (instancetype)init { if (self = [super init]) { _loginAccessBlock = nil; _loginQueue = dispatch_queue_create("login.queue", DISPATCH_QUEUE_SERIAL); _loginOperations = [[NSMutableArray alloc] init]; } return self; } - (NSOperation *)triggerLogin:(TAPILoginCompletionBlock)loginBlock { TAPILoginOperation *op = [[TAPILoginOperation alloc] init]; op.client = self; if (loginBlock) { __unsafe_unretained TAPILoginOperation *opRef = op; op.completionBlock = ^{ TAPIClient *client = opRef.client; if (client) { dispatch_sync(client->_loginQueue, ^{ [client->_loginOperations removeObject:opRef]; }); } const BOOL didSucceed = opRef.didSucceed; dispatch_async(dispatch_get_main_queue(), ^{ loginBlock(didSucceed); }); }; } dispatch_sync(_loginQueue, ^{ for (NSOperation *otherOp in self->_loginOperations) { [op addDependency:otherOp]; } [self->_loginOperations addObject:op]; }); [[NSOperationQueue mainQueue] addOperation:op]; return op; } - (TNLRequestOperation *)_tapi_startRequest:(TAPIRequest *)request delegate:(id<TNLRequestDelegate>)delegate completion:(TAPIRequestCompletionBlock)completion { Class requestClass = [request class]; TNLMutableRequestConfiguration *config = [[requestClass configuration] mutableCopy]; config.retryPolicyProvider = [requestClass retryPolicyProvider]; TAPIOperationContext *context = [[TAPIOperationContext alloc] init]; context.loginOperation = [self triggerLogin:NULL]; context.completionBlock = completion; TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request responseClass:[requestClass responseClass] ?: [TAPIResponse class] configuration:config delegate:delegate]; if (context.loginOperation) { [op addDependency:context.loginOperation]; } op.context = context; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:op]; return op; } - (TNLRequestOperation *)startRequest:(TAPIRequest *)request completion:(TAPIRequestCompletionBlock)completion { return [self _tapi_startRequest:request delegate:self completion:completion]; } - (TNLRequestOperation *)startRequest:(TAPIRequest *)request delegate:(id<TNLRequestDelegate>)delegate { return [self _tapi_startRequest:request delegate:delegate completion:nil]; } #pragma mark TNLRequestHydrater - (dispatch_queue_t)tnl_delegateQueueForRequestOperation:(TNLRequestOperation *)op { return _loginQueue; } - (void)tnl_requestOperation:(TNLRequestOperation *)op hydrateRequest:(TAPIRequest *)request completion:(TNLRequestHydrateCompletionBlock)complete { complete([TNLHTTPRequest HTTPRequestWithRequest:request], nil); } - (void)tnl_requestOperation:(TNLRequestOperation *)op authorizeURLRequest:(NSURLRequest *)URLRequest completion:(TNLAuthorizeCompletionBlock)completion { // This method is externally exposed so it could be called on any thread. // Ensure we are on the correct queue. if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) != dispatch_queue_get_label(_loginQueue)) { dispatch_async(_loginQueue, ^{ [self tnl_requestOperation:op authorizeURLRequest:URLRequest completion:completion]; }); return; } NSString *consumerKey = self.oauthConsumerKey; NSString *consumerSecret = self.oauthConsumerSecret; if (!consumerKey || !consumerSecret) { completion(nil, [NSError errorWithDomain:TAPIOperationErrorDomain code:TAPIOperationErrorCodeMissingConsumerCredentials userInfo:nil]); return; } NSString *oauthSecret = _oauthAccessSecret; NSString *oauthToken = _oauthAccessToken; if (!oauthSecret || !oauthToken) { completion(nil, [NSError errorWithDomain:TAPIOperationErrorDomain code:TAPIOperationErrorCodeMissingAccessCredentials userInfo:nil]); return; } TAPIRequest *request = (id)op.originalRequest; TAPIOperationContext *context = op.context; NSString *method = TNLRequestGetHTTPMethod(URLRequest); NSString *baseURLString = [request baseURLString]; TNLMutableParameterCollection *params = [[request parameters] mutableCopy] ?: [[TNLMutableParameterCollection alloc] init]; NSMutableDictionary *oauthParams = [[NSMutableDictionary alloc] init]; oauthParams[@"oauth_nonce"] = context.oauthNonce; oauthParams[@"oauth_timestamp"] = [NSString stringWithFormat:@"%qd", (long long)[[NSDate date] timeIntervalSince1970]]; oauthParams[@"oauth_signature_method"] = @"HMAC-SHA1"; oauthParams[@"oauth_version"] = @"1.0"; oauthParams[@"oauth_consumer_key"] = self.oauthConsumerKey; oauthParams[@"oauth_token"] = oauthToken; [params addParametersDirectlyFromDictionary:oauthParams combineRepeatingKeys:NO]; NSString *oauthString = [NSString stringWithFormat:@"%@&%@&%@", method, TNLURLEncodeString(baseURLString), TNLURLEncodeString([params stableURLEncodedStringValue])]; NSString *signingKey = [NSString stringWithFormat:@"%@&%@", self.oauthConsumerSecret, oauthSecret]; NSData *signatureBytes = HMAC_SHA1(oauthString, signingKey); NSString *signatureBase64 = [signatureBytes base64EncodedStringWithOptions:0]; oauthParams[@"oauth_signature"] = signatureBase64; NSMutableArray *authHeaderItems = [NSMutableArray array]; for (NSString *key in oauthParams) { NSString *value = oauthParams[key]; [authHeaderItems addObject:[NSString stringWithFormat:@"%@=\"%@\"", key, TNLURLEncodeString(value)]]; } NSString *authString = [NSString stringWithFormat:@"OAuth %@", [authHeaderItems componentsJoinedByString:@", "]]; completion(authString, nil); } #pragma mark TNLRequestDelegate - (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteWithResponse:(TAPIResponse *)response { TNLAssert([op.originalRequest isKindOfClass:[TAPIRequest class]]); TNLAssert([response isKindOfClass:[TAPIResponse class]]); TAPIOperationContext *context = op.context; TAPIRequestCompletionBlock completion = context.completionBlock; if (completion) { assert([NSThread isMainThread]); completion(response); } } #pragma mark Internal - (void)_tapi_login:(TAPILoginCompletionBlock)completion { dispatch_async(_loginQueue, ^{ if (self->_oauthAccessSecret && self->_oauthAccessToken) { completion(YES); return; } TAPILoginAccessBlock loginBlock = self.loginAccessBlock; if (!loginBlock) { completion(NO); return; } dispatch_async(dispatch_get_main_queue(), ^{ loginBlock(^(NSString *token, NSString *secret) { dispatch_async(self->_loginQueue, ^{ self->_oauthAccessToken = [token copy]; self->_oauthAccessSecret = [secret copy]; completion((token != nil && secret != nil)); }); }); }); }); } @end @implementation TAPILoginOperation { volatile atomic_bool _isFinished; volatile atomic_bool _isExecuting; volatile atomic_bool _didStart; volatile atomic_bool _didSucceed; } - (instancetype)init { self = [super init]; if (self) { atomic_init(&_isFinished, false); atomic_init(&_isExecuting, false); atomic_init(&_didStart, false); atomic_init(&_didSucceed, false); } return self; } - (BOOL)isAsynchronous { return YES; } - (BOOL)isConcurrent { return YES; } - (BOOL)isExecuting { return atomic_load(&_isExecuting); } - (BOOL)isFinished { return atomic_load(&_isFinished); } - (BOOL)didSucceed { return atomic_load(&_didSucceed); } - (void)start { tnl_defer(^{ atomic_store(&(self->_didStart), true); }); [self willChangeValueForKey:@"isExecuting"]; atomic_store(&_isExecuting, true); [self didChangeValueForKey:@"isExecuting"]; TAPIClient *client = self.client; if (!client) { [self complete]; return; } [client _tapi_login:^(BOOL loginSucceeded) { atomic_store(&self->_didSucceed, loginSucceeded); [self complete]; }]; } - (void)complete { if (false == atomic_load(&self->_didStart)) { // Completed synchronously, don't want to mess up "isAsynchronous" behavior dispatch_async(dispatch_get_main_queue(), ^{ [self complete]; }); return; } [self willChangeValueForKey:@"isFinished"]; [self willChangeValueForKey:@"isExecuting"]; atomic_store(&self->_isExecuting, false); atomic_store(&self->_isFinished, true); [self didChangeValueForKey:@"isExecuting"]; [self didChangeValueForKey:@"isFinished"]; } @end