/* |
File: AppDelegate.m |
Abstract: Main app controller. |
Version: 1.1 |
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
|
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
|
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
|
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
|
*/ |
|
#import "AppDelegate.h" |
|
#import "WebViewController.h" |
|
#import "CredentialsManager.h" |
|
#import "CustomHTTPProtocol.h" |
|
#import "ThreadInfo.h" |
|
#include <pthread.h> // for pthread_threadid_np |
|
@interface AppDelegate () <UIApplicationDelegate, WebViewControllerDelegate, CustomHTTPProtocolDelegate> |
|
@property (nonatomic, strong, readwrite) CredentialsManager * credentialsManager; |
|
/*! For threadInfoByThreadID, each key is an NSNumber holding a thread ID and each |
value is a ThreadInfo object. The dictionary is protected by @synchronized on |
the app delegate object itself. |
|
In the debugger you can dump this info with: |
|
(lldb) po [[[UIApplication sharedApplication] delegate] threadInfoByThreadID] |
*/ |
|
@property (atomic, strong, readwrite) NSMutableDictionary * threadInfoByThreadID; |
@property (atomic, assign, readwrite) NSUInteger nextThreadNumber; ///< Protected by @synchronized on the delegate object. |
|
@end |
|
@implementation AppDelegate |
|
@synthesize window = _window; // synthesis required because property is declared in UIApplicationDelegate protocol |
|
static BOOL sAppDelegateLoggingEnabled = YES; |
|
static NSTimeInterval sAppStartTime; // since reference date |
|
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions |
{ |
#pragma unused(application) |
#pragma unused(launchOptions) |
WebViewController * webViewController; |
|
assert(self.window != nil); |
|
sAppStartTime = [NSDate timeIntervalSinceReferenceDate]; |
|
self.credentialsManager = [[CredentialsManager alloc] init]; |
|
// Prepare the globals needed by our logging code. The call to -threadInfoForCurrentThread |
// sets up the main thread's thread info record and ensures it has a thread number of 0. |
|
self.threadInfoByThreadID = [[NSMutableDictionary alloc] init]; |
(void) [self threadInfoForCurrentThread]; |
|
// Start up the core code. Change the if expression to NO to disable the CustomHTTPProtocol for |
// comparative testing and so on. |
|
[CustomHTTPProtocol setDelegate:self]; |
if (YES) { |
[CustomHTTPProtocol start]; |
} |
|
// Create the web view controller and set up the UI. We do this after setting |
// up the core code in case this triggers any HTTP requests. |
// |
// By default the Test button is not shown because this sample is focused on UIWebView. |
// If you want to runs tests with NSURL{Session,Connection}, change the if expression to |
// show the Test button and then configure the test by changing the code in -testAction:. |
|
webViewController = [[UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle bundleForClass:[self class]]] instantiateViewControllerWithIdentifier:@"webView"]; |
assert(webViewController != nil); |
webViewController.delegate = self; |
if (NO) { |
webViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Test" style:UIBarButtonItemStyleBordered target:self action:@selector(testAction:)]; |
} |
[((UINavigationController *) self.window.rootViewController) pushViewController:webViewController animated:NO]; |
|
[self.window makeKeyAndVisible]; |
|
return YES; |
} |
|
- (ThreadInfo *)threadInfoForCurrentThread |
{ |
int junk; |
uint64_t tid; |
NSNumber * tidObj; |
ThreadInfo * result; |
|
// Get the thread ID and box it for use as a dictionary key. |
|
junk = pthread_threadid_np(pthread_self(), &tid); |
#pragma unused(junk) // quietens analyser in the Release build |
assert(junk == 0); |
tidObj = @(tid); |
|
// Look up the thread info using that key. |
|
@synchronized (self) { |
result = self.threadInfoByThreadID[tidObj]; |
} |
|
// If we didn't find one, create it. We drop the @synchronized while doing this because |
// it might take a while; in theory no one else should be able to add this thread into |
// the dictionary (because threads only add themselves) so we just assert that this |
// hasn't happened. |
// |
// Also note that, because self.nextThreadNumber accesses must be protected by the |
// @synchronized, we actually created the ThreadInfo object inside the @synchronized |
// block. That shouldn't be a problem because -[ThreadInfo initXxx] is trivial. |
|
if (result == nil) { |
ThreadInfo * newThreadInfo; |
char threadName[256]; |
NSString * threadNameObj; |
|
if ( (pthread_getname_np(pthread_self(), threadName, sizeof(threadName)) == 0) && (threadName[0] != 0) ) { |
// We got a name and it's not empty. |
threadNameObj = [[NSString alloc] initWithUTF8String:threadName]; |
} else if (pthread_main_np()) { |
threadNameObj = @"-main-"; |
} else { |
threadNameObj = @"-unnamed-"; |
} |
assert(threadNameObj != nil); |
|
@synchronized (self) { |
assert(self.threadInfoByThreadID[tidObj] == nil); |
|
newThreadInfo = [[ThreadInfo alloc] initWithThreadID:tid number:self.nextThreadNumber name:threadNameObj]; |
self.nextThreadNumber += 1; |
|
self.threadInfoByThreadID[tidObj] = newThreadInfo; |
result = newThreadInfo; |
} |
} |
|
return result; |
} |
|
/*! Our logging core, called by various logging routines, each with a unique prefix. May be called |
* by any thread. |
* \param prefix A prefix to to insert into the log; must not be nil; if non-empty, should include a trailing space. |
* \param format A standard NSString-style format string. |
* \param arguments Arguments for that format string. |
*/ |
|
- (void)logWithPrefix:(NSString *)prefix format:(NSString *)format arguments:(va_list)arguments |
{ |
assert(prefix != nil); |
assert(format != nil); |
|
if (sAppDelegateLoggingEnabled) { |
NSTimeInterval now; |
ThreadInfo * threadInfo; |
NSString * str; |
char elapsedStr[16]; |
|
now = [NSDate timeIntervalSinceReferenceDate]; |
|
threadInfo = [self threadInfoForCurrentThread]; |
|
str = [[NSString alloc] initWithFormat:format arguments:arguments]; |
assert(str != nil); |
|
snprintf(elapsedStr, sizeof(elapsedStr), "+%.1f", (now - sAppStartTime)); |
|
fprintf(stderr, "%3zu %s %s%s\n", (size_t) threadInfo.number, elapsedStr, [prefix UTF8String], [str UTF8String]); |
} |
} |
|
- (void)customHTTPProtocol:(CustomHTTPProtocol *)protocol logWithFormat:(NSString *)format arguments:(va_list)arguments |
{ |
NSString * prefix; |
|
// protocol may be nil |
assert(format != nil); |
|
if (protocol == nil) { |
prefix = @"protocol "; |
} else { |
prefix = [NSString stringWithFormat:@"protocol %p ", protocol]; |
} |
[self logWithPrefix:prefix format:format arguments:arguments]; |
} |
|
- (BOOL)webViewController:(WebViewController *)controller addTrustedAnchor:(SecCertificateRef)anchor error:(NSError *__autoreleasing *)errorPtr |
{ |
#pragma unused(controller) |
assert(controller != nil); |
assert(anchor != NULL); |
// errorPtr may be NULL |
#pragma unused(errorPtr) |
assert([NSThread isMainThread]); |
|
[self.credentialsManager addTrustedAnchor:anchor]; |
return YES; |
} |
|
- (void)webViewController:(WebViewController *)controller logWithFormat:(NSString *)format arguments:(va_list)arguments |
{ |
#pragma unused(controller) |
assert(controller != nil); |
assert(format != nil); |
assert([NSThread isMainThread]); |
|
[self logWithPrefix:@"web view " format:format arguments:arguments]; |
} |
|
/*! Called by the test subsystem (see below) to log various bits of information. |
* Will be called on the main thread. |
* \param format A standard NSString-style format string; will not be nil. |
*/ |
|
- (void)testLogWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2) |
{ |
va_list arguments; |
|
assert(format != nil); |
|
va_start(arguments, format); |
[self logWithPrefix:@"test " format:format arguments:arguments]; |
va_end(arguments); |
} |
|
- (BOOL)customHTTPProtocol:(CustomHTTPProtocol *)protocol canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace |
{ |
assert(protocol != nil); |
#pragma unused(protocol) |
assert(protectionSpace != nil); |
|
// We accept any server trust authentication challenges. |
|
return [[protectionSpace authenticationMethod] isEqual:NSURLAuthenticationMethodServerTrust]; |
} |
|
- (void)customHTTPProtocol:(CustomHTTPProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge |
{ |
OSStatus err; |
NSURLCredential * credential; |
SecTrustRef trust; |
SecTrustResultType trustResult; |
|
// Given our implementation of -customHTTPProtocol:canAuthenticateAgainstProtectionSpace:, this method |
// is only called to handle server trust authentication challenges. It evaluates the trust based on |
// both the global set of trusted anchors and the list of trusted anchors returned by the CredentialsManager. |
|
assert(protocol != nil); |
assert(challenge != nil); |
assert([[[challenge protectionSpace] authenticationMethod] isEqual:NSURLAuthenticationMethodServerTrust]); |
assert([NSThread isMainThread]); |
|
credential = nil; |
|
// Extract the SecTrust object from the challenge, apply our trusted anchors to that |
// object, and then evaluate the trust. If it's OK, create a credential and use |
// that to resolve the authentication challenge. If anything goes wrong, resolve |
// the challenge with nil, which continues without a credential, which causes the |
// connection to fail. |
|
trust = [[challenge protectionSpace] serverTrust]; |
if (trust == NULL) { |
assert(NO); |
} else { |
err = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef) self.credentialsManager.trustedAnchors); |
if (err != noErr) { |
assert(NO); |
} else { |
err = SecTrustSetAnchorCertificatesOnly(trust, false); |
if (err != noErr) { |
assert(NO); |
} else { |
err = SecTrustEvaluate(trust, &trustResult); |
if (err != noErr) { |
assert(NO); |
} else { |
if ( (trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified) ) { |
credential = [NSURLCredential credentialForTrust:trust]; |
assert(credential != nil); |
} |
} |
} |
} |
} |
|
[protocol resolveAuthenticationChallenge:challenge withCredential:credential]; |
} |
|
// We don't need to implement -customHTTPProtocol:didCancelAuthenticationChallenge: because we always resolve |
// the challenge synchronously within -customHTTPProtocol:didReceiveAuthenticationChallenge:. |
|
#pragma mark Test Button |
|
/*! Called when the user taps of the (optional) Test button in the nav bar. This kicks off a various |
* tests, selectable at compile time by changing the if expressions. |
* \param sender The object that sent this action. |
*/ |
|
- (void)testAction:(id)sender |
{ |
#pragma unused(sender) |
if (NO) { |
[self testNSURLConnection]; |
} |
if (YES) { |
[self testNSURLSession]; |
} |
} |
|
#pragma mark NSURLSession test |
|
/*! This routine kicks off a vanilla NSURLSession task, as opposed to the UIWebView test shown by the |
* main app. This is useful because UIWebView uses NSURLConnection (actually, the private CFNetwork |
* API that underlies NSURLConnection, CFURLConnection) in a unique way, so it's important to test |
* your code with both UIWebView and NSURLSession. |
*/ |
|
- (void)testNSURLSession |
{ |
[self testLogWithFormat:@"start (NSURLSession)"]; |
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://www.apple.com/"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { |
#pragma unused(data) |
if (error != nil) { |
[self testLogWithFormat:@"error:%@ / %d", [error domain], (int) [error code]]; |
} else { |
[self testLogWithFormat:@"success:%zd / %@", (ssize_t) [(NSHTTPURLResponse *) response statusCode], [response URL]]; |
} |
}] resume]; |
} |
|
#pragma mark NSURLConnection test |
|
/*! This routine kicks off a vanilla NSURLConnection, as opposed to the UIWebView test shown by the |
* main app. This is useful because UIWebView uses NSURLConnection (actually, the private CFNetwork |
* API that underlies NSURLConnection, CFURLConnection) in a unique way, so it's important to test |
* your code with both UIWebView and NSURLConnection. |
*/ |
|
- (void)testNSURLConnection |
{ |
[self testLogWithFormat:@"start (NSURLConnection)"]; |
(void) [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.apple.com/"]] delegate:self]; |
} |
|
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response |
{ |
#pragma unused(connection) |
[self testLogWithFormat:@"willSendRequest:%@ redirectResponse:%@", [request URL], [response URL]]; |
return request; |
} |
|
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response |
{ |
#pragma unused(connection) |
#pragma unused(response) |
[self testLogWithFormat:@"didReceiveResponse:%zd / %@", (ssize_t) [(NSHTTPURLResponse *) response statusCode], [response URL]]; |
} |
|
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data |
{ |
#pragma unused(connection) |
#pragma unused(data) |
[self testLogWithFormat:@"didReceiveData:%zu", (size_t) [data length]]; |
} |
|
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse |
{ |
#pragma unused(connection) |
[self testLogWithFormat:@"willCacheResponse:%@", [[cachedResponse response] URL]]; |
return cachedResponse; |
} |
|
- (void)connectionDidFinishLoading:(NSURLConnection *)connection |
{ |
#pragma unused(connection) |
[self testLogWithFormat:@"connectionDidFinishLoading"]; |
} |
|
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error |
{ |
#pragma unused(connection) |
#pragma unused(error) |
[self testLogWithFormat:@"didFailWithError:%@ / %d", [error domain], (int) [error code]]; |
} |
|
@end |