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