Last active
August 29, 2015 14:14
-
-
Save jkereako/70df01d95019ae2e9765 to your computer and use it in GitHub Desktop.
Classes for handling HTTP requests and credential storage.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ADAuthenticationModel.h | |
// NetworkAuthentication | |
// | |
// Created by Jeffrey Kereakoglow on 3/11/15. | |
// Copyright (c) 2015 Alexis Digital. All rights reserved. | |
// | |
@import Foundation; | |
@interface ADAuthenticationModel : NSObject | |
@property (nonatomic) NSString *username; | |
@property (nonatomic) NSString *password; | |
@property (nonatomic) NSString *authenticationToken; | |
@property (nonatomic) NSURLCredential *credential; | |
@property (nonatomic, readonly) NSString *urlString; | |
@property (nonatomic, readonly) NSURLProtectionSpace *urlProtectionSpace; | |
@property (nonatomic, getter=isAuthenticated) BOOL authenticated; | |
- (instancetype)initWithURLString:(NSString *)urlString __attribute((nonnull)) NS_DESIGNATED_INITIALIZER; | |
- (NSString *)hashedStringFromString:(NSString *)stringToHash __attribute((nonnull)); | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ADAuthenticationModel.m | |
// NetworkAuthentication | |
// | |
// Created by Jeffrey Kereakoglow on 3/11/15. | |
// Copyright (c) 2015 Alexis Digital. All rights reserved. | |
// | |
#import "ADAuthenticationModel.h" | |
#import <CommonCrypto/CommonDigest.h> | |
@interface ADAuthenticationModel () | |
- (NSURLCredential *)credentialFromKeychainForProtectionSpace:(NSURLProtectionSpace *)protectionSpace __attribute((nonnull)); | |
@end | |
@implementation ADAuthenticationModel | |
@synthesize credential = _credential; | |
- (instancetype)initWithURLString:(NSString *)urlString { | |
self = [super init]; | |
if (self) { | |
_urlString = urlString; | |
} | |
return self; | |
} | |
#pragma mark - Getters | |
- (NSString *)username { | |
NSAssert(self.credential, @"\n\n ERROR in %s: The property \"_credential\" is nil.\n\n", __PRETTY_FUNCTION__); | |
return self.credential.user; | |
} | |
- (NSString *)password { | |
NSAssert(self.credential, @"\n\n ERROR in %s: The property \"_credential\" is nil.\n\n", __PRETTY_FUNCTION__); | |
return self.credential.password; | |
} | |
- (NSURLProtectionSpace *)urlProtectionSpace { | |
NSAssert(self.urlString, @"\n\n ERROR in %s: The property \"_urlString\" is nil.\n\n", __PRETTY_FUNCTION__); | |
static NSURLProtectionSpace *_urlProtectionSpace; | |
static dispatch_once_t onceToken; | |
dispatch_once(&onceToken, ^{ | |
NSURL *url = [NSURL URLWithString:self.urlString]; | |
if (url) { | |
_urlProtectionSpace = [[NSURLProtectionSpace alloc] initWithHost:url.host | |
port:[url.port integerValue] | |
protocol:url.scheme | |
realm:nil | |
authenticationMethod:NSURLAuthenticationMethodDefault]; | |
} | |
else { | |
NSAssert(NO, @"\n\n UNEXPECTED BEHAVIOR in %s", __PRETTY_FUNCTION__); | |
} | |
}); | |
return _urlProtectionSpace; | |
} | |
/** | |
Fetch an NSURLCredential from the keychain and set it——it's unorthodox to set a property in a getter; I'm aware——to the property, _credential. | |
@returns NSURLCredential * or nil | |
@since 1.0 | |
@author Jeff Kereakoglow | |
*/ | |
- (NSURLCredential *)credential { | |
// Normal getter behavior | |
if (_credential) { | |
return _credential; | |
} | |
// Fetch the credential from the keychain, if it exists. | |
return [self credentialFromKeychainForProtectionSpace:self.urlProtectionSpace]; | |
} | |
#pragma mark - Setters | |
/** | |
Fetch an NSURLCredential from the keychain and set it to the property _credential. | |
@param credential NSURLCredential * | |
@since 1.0 | |
@author Jeff Kereakoglow | |
*/ | |
- (void)setCredential:(NSURLCredential *)credential { | |
NSAssert(credential, @"\n\n ERROR in %s: The parameter \"credential\" is nil.\n\n", __PRETTY_FUNCTION__); | |
NSAssert(self.urlProtectionSpace, @"\n\n ERROR in %s: The property \"_urlProtectionSpace\" is nil.\n\n", __PRETTY_FUNCTION__); | |
// Assign the credential to the Keychain | |
[[NSURLCredentialStorage sharedCredentialStorage] setCredential:credential | |
forProtectionSpace:self.urlProtectionSpace]; | |
_credential = credential; | |
} | |
#pragma mark - Private helpers | |
/** | |
Hashes a string using SHA256. | |
@param stringToHash NSString * | |
@returns The hashed string | |
@since 1.0 | |
@see https://github.com/mdznr/iOS-Passcode/blob/master/Passcode/NSString%2Bsha256.m#L61-L75 | |
@author Jeff Kereakoglow | |
*/ | |
- (NSString *)hashedStringFromString:(NSString *)stringToHash { | |
NSMutableString *hashedString; | |
const char *str = [stringToHash UTF8String]; | |
unsigned char result[CC_SHA256_DIGEST_LENGTH]; | |
CC_SHA256(str, (CC_LONG) strlen(str), result); | |
hashedString = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2]; | |
for (NSUInteger i = 0; i != CC_SHA256_DIGEST_LENGTH; ++ i) { | |
[hashedString appendFormat:@"%02x",result[i]]; | |
} | |
return [NSString stringWithString:hashedString]; | |
} | |
/** | |
Fetches from the keychian a single @c NSURLCredential for a given @c NSURLProtectionSpace. | |
@param protectionSpace NSURLProtectionSpace * | |
@returns The @c NSURLCredential or nil if no such credential exists | |
@since 1.0 | |
@see https://github.com/mdznr/iOS-Passcode/blob/master/Passcode/NSString%2Bsha256.m#L61-L75 | |
@author Jeff Kereakoglow | |
*/ | |
- (NSURLCredential *)credentialFromKeychainForProtectionSpace:(NSURLProtectionSpace *)protectionSpace { | |
NSURLCredential *credential; | |
NSDictionary *credentials = [[NSURLCredentialStorage sharedCredentialStorage] credentialsForProtectionSpace:protectionSpace]; | |
if (credentials.count) { | |
credential = [credentials.objectEnumerator nextObject]; | |
if (!credential) { | |
NSAssert(NO, @"\n\n UNEXPECTED BEHAVIOR in %s.\n\n", __PRETTY_FUNCTION__); | |
} | |
} | |
return credential; | |
} | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ADWebService.h | |
// NetworkAuthentication | |
// | |
// Created by Jeffrey Kereakoglow on 3/10/15. | |
// Copyright (c) 2015 Alexis Digital. All rights reserved. | |
// | |
@import Foundation; | |
@interface ADWebService : NSObject<NSURLSessionDataDelegate> | |
+ (instancetype)webServiceWithURLString:(NSString *)urlString __attribute((nonnull)); | |
@property (nonatomic, readonly) NSURL *url; | |
@property (nonatomic, readonly) NSURLProtectionSpace *urlProtectionSpace; | |
@property (nonatomic, readonly) NSURLCredential *urlCredential; | |
- (id)collectionFromJSONData:(NSData *)data error:(__autoreleasing NSError **)error __attribute((nonnull)); | |
- (BOOL)sendHTTPFormData:(NSData *)formData completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler __attribute((nonnull)); | |
- (BOOL)sendJSONData:(NSData *)jsonData completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler __attribute((nonnull)); | |
@end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ADWebService.m | |
// NetworkAuthentication | |
// | |
// Created by Jeffrey Kereakoglow on 3/10/15. | |
// Copyright (c) 2015 Alexis Digital. All rights reserved. | |
// | |
#import "ADWebService.h" | |
@import UIKit.UIApplication; | |
static NSURLSession *urlSession; | |
@interface ADWebService () | |
@property (nonatomic, readonly) NSMutableURLRequest *mutableURLRequest; | |
- (instancetype)initWithURLString:(NSString *)urlString __attribute((nonnull)) NS_DESIGNATED_INITIALIZER; | |
- (BOOL)fetchResourceWithQueryString:(NSString *)queryString completionHandler:(void (^)(NSURLRequest *request,id response, NSError *error))completionHandler __attribute((nonnull)); | |
- (BOOL)sendData:(NSData *)data contentType:(NSString *)contentType completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler __attribute((nonnull)); | |
- (void)handleDataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler; | |
@end | |
@implementation ADWebService | |
#pragma mark - | |
+ (instancetype)webServiceWithURLString:(NSString *)urlString { | |
ADWebService *instance = [[[self class] alloc] initWithURLString:urlString]; | |
if(!urlSession) { | |
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; | |
// Customize NSURLSession | |
config.URLCredentialStorage = [NSURLCredentialStorage sharedCredentialStorage]; | |
config.allowsCellularAccess = NO; | |
// NSURLSessionConfiguration defaults. These must not be overridden | |
config.timeoutIntervalForRequest = 30.0; | |
config.timeoutIntervalForResource = 60.0; | |
// This limit is per-session. Keep it high so we can make concurrent | |
// connections on multiple threads. | |
config.HTTPMaximumConnectionsPerHost = 5; | |
urlSession = [NSURLSession sessionWithConfiguration:config | |
delegate:instance | |
delegateQueue:nil]; | |
} | |
return instance; | |
} | |
- (instancetype)initWithURLString:(NSString *)urlString { | |
self = [super init]; | |
if (self) { | |
_url = [NSURL URLWithString:urlString]; | |
_urlProtectionSpace = [[NSURLProtectionSpace alloc] initWithHost:_url.host | |
port:[_url.port integerValue] | |
protocol:_url.scheme | |
realm:nil | |
authenticationMethod:NSURLAuthenticationMethodDefault]; | |
} | |
return self; | |
} | |
#pragma mark - Getters | |
- (NSURLCredential *)urlCredential { | |
NSAssert(self.urlProtectionSpace, @"\n\n ERROR in %s: The property \"_urlProtectionSpace\" is nil.\n\n", __PRETTY_FUNCTION__); | |
NSURLCredential *urlCredential; | |
NSDictionary *credentials; | |
credentials = [[NSURLCredentialStorage sharedCredentialStorage] credentialsForProtectionSpace:self.urlProtectionSpace]; | |
urlCredential = [credentials.objectEnumerator nextObject]; | |
#if DEBUG | |
NSLog(@"\n\n INFO: Username: %@\n Password: %@.\n\n", urlCredential.user, urlCredential.password); | |
#endif | |
return urlCredential; | |
} | |
- (NSMutableURLRequest *)mutableURLRequest { | |
NSAssert(self.url, @"\n\n ERROR in %s: The property \"_url\" is nil.\n\n", __PRETTY_FUNCTION__); | |
NSMutableURLRequest *mutableURLRequest; | |
mutableURLRequest = [NSMutableURLRequest requestWithURL:self.url | |
cachePolicy:NSURLRequestUseProtocolCachePolicy | |
timeoutInterval:60.0]; | |
[mutableURLRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"]; | |
[mutableURLRequest setHTTPMethod:@"GET"]; | |
return mutableURLRequest; | |
} | |
#pragma mark - | |
- (id)collectionFromJSONData:(NSData *)data error:(__autoreleasing NSError **)error { | |
id result = [NSJSONSerialization JSONObjectWithData:data | |
options:NSJSONReadingMutableContainers | |
error:error]; | |
return result; | |
} | |
#pragma mark - NSURLSessionTaskDelegate | |
// Handle authentication challenges. | |
// This delegate will be invoked happen for every TLS connection which has not | |
// been verified by a certificate authority (CA). | |
- (void)URLSession:(__unused NSURLSession *)session didReceiveChallenge:(__unused NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { | |
// Allow connection if conditions are met. | |
if ([challenge.protectionSpace.host isEqualToString:self.urlProtectionSpace.host]) { | |
NSURLCredential *credential = [NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust]; | |
completionHandler(NSURLSessionAuthChallengeUseCredential, credential); | |
} | |
// Deny connection | |
else { | |
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); | |
} | |
} | |
#pragma mark - NFWebServiceDelegate | |
- (BOOL)sendJSONData:(NSData *)jsonData completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler { | |
return [self sendData:jsonData | |
contentType:@"application/json" | |
completionHandler:completionHandler]; | |
} | |
- (BOOL)sendHTTPFormData:(NSData *)formData completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler { | |
return [self sendData:formData | |
contentType:@"application/x-www-form-urlencoded" | |
completionHandler:completionHandler]; | |
} | |
#pragma mark - | |
- (BOOL)sendData:(NSData *)data contentType:(NSString *)contentType completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler { | |
NSMutableURLRequest *mutableURLRequest = self.mutableURLRequest; | |
[mutableURLRequest setHTTPMethod:@"POST"]; | |
[mutableURLRequest setValue:contentType | |
forHTTPHeaderField:@"Content-Type"]; | |
[mutableURLRequest setHTTPBody:data ]; | |
[self handleDataTaskWithRequest:mutableURLRequest | |
completionHandler:completionHandler]; | |
return YES; | |
} | |
- (BOOL)fetchResourceWithQueryString:(__unused NSString *)queryString completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler { | |
NSMutableURLRequest *mutableURLRequest = self.mutableURLRequest; | |
if (queryString) { | |
mutableURLRequest.URL = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", | |
self.url, queryString]]; | |
} | |
[self handleDataTaskWithRequest:mutableURLRequest | |
completionHandler:completionHandler]; | |
return YES; | |
} | |
- (void)handleDataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *request, id response, NSError *error))completionHandler { | |
NSAssert(urlSession, @"\n\n ERROR in %s: The property \"_urlSession\" is nil.\n\n", __PRETTY_FUNCTION__); | |
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES; | |
[[urlSession dataTaskWithRequest:request | |
completionHandler: | |
^(NSData *data, NSURLResponse *response, NSError *error) { | |
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO; | |
// If there was an error, then report it and execute the completion | |
// handler | |
if (error) { | |
completionHandler(request, response, error); | |
} | |
// Else, process the response. | |
else { | |
NSError *newError; | |
NSDictionary *vessels; | |
NSString *errorDescription; | |
NSInteger statusCode = 0; | |
// Sanity check | |
if ([response isKindOfClass:[NSHTTPURLResponse class]]) { | |
// Get the HTTP status code | |
statusCode = ((NSHTTPURLResponse *)response).statusCode; | |
switch (statusCode) { | |
case 200: | |
break; | |
case 400: | |
errorDescription = NSLocalizedString(@"webService.httpResponseCode.400.badRequest", @"HTTP: 400"); | |
break; | |
case 401: | |
errorDescription = NSLocalizedString(@"webService.httpResponseCode.401.unauthorizedAccess", @"HTTP: 401"); | |
break; | |
case 403: | |
errorDescription = NSLocalizedString(@"webService.httpResponseCode.403.forbidden", @"HTTP: 403"); | |
break; | |
case 404: | |
errorDescription = NSLocalizedString(@"webService.httpResponseCode.404.notFound", @"HTTP: 404"); | |
break; | |
case 500: | |
errorDescription = NSLocalizedString(@"webService.httpResponseCode.500.internalServerError", @"HTTP: 500"); | |
break; | |
default: | |
NSAssert(NO, @"\n\n UNEXEPECTED BEHAVIOR in %s\n\n", __PRETTY_FUNCTION__); | |
break; | |
} | |
if (errorDescription) { | |
vessels = @{ NSLocalizedDescriptionKey : errorDescription}; | |
newError = [NSError errorWithDomain:[NSBundle mainBundle].bundleIdentifier | |
code:statusCode | |
userInfo:vessels]; | |
} | |
} | |
// If an error was generated from an HTTP status code, then call | |
// the completion handler | |
if (newError) { | |
completionHandler(request, response, newError); | |
} | |
// Else, process the response from the server normally. | |
else { | |
id result = [self collectionFromJSONData:data | |
error:&newError]; | |
// Was there an error deserialized JSON? | |
if (newError) { | |
completionHandler(request, response, newError); | |
} | |
// Everything went smoothly. | |
else { | |
completionHandler(request, result, newError); | |
} | |
} | |
} | |
}] resume]; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Description
These are 2 classes which I use to handle HTTP requests and to store credentials.
ADWebService
: Handles HTTP requests. It also includes helpers for sending form data or JSON data.ADAuthenticationModel
: Encapsulates routines related to credential storage. This is application specific. It's likely that you will not be able to use this class as-is.Previously, I used AFNetworking to handle my networking needs, but since my discovery of NSURLSession, I've abandoned AFNetworking. NSURLSession is really, really easy to use compared to NSURLRequest.
Usage
This is meant to be used with another class called
WebServiceClient
whose sole purpose is to construct web requests in the format expected by the server.The client is application specific. For example, you would create
TwitterWebServiceClient
which would contain methods specific to the Twitter API and then pass the object to the web service to actually make the call.Credit
I got the idea of separating the web service and web service client from Objc.io Issue #10. In that issue, there exists the class
PodsWebService
and it's client class,Importer
.