Procházet zdrojové kódy

Merge pull request #434 from mirokuratczyk/master

iOS Library: TunneledWebView sample app
Miro před 8 roky
rodič
revize
21d2273546
48 změnil soubory, kde provedl 3795 přidání a 0 odebrání
  1. 217 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.h
  2. 929 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m
  3. 61 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCacheStoragePolicy.h
  4. 124 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCacheStoragePolicy.m
  5. 64 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCanonicalRequest.h
  6. 459 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCanonicalRequest.m
  7. 91 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPQNSURLSessionDemux.h
  8. 332 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPQNSURLSessionDemux.m
  9. 90 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/README.md
  10. 655 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/project.pbxproj
  11. 7 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  12. 113 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/xcshareddata/xcschemes/TunneledWebView.xcscheme
  13. 181 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/AppDelegate.swift
  14. 158 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Contents.json
  15. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-40.png
  16. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png
  17. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png
  18. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
  19. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
  20. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-72.png
  21. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png
  22. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-76.png
  23. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
  24. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png
  25. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png
  26. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png
  27. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
  28. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
  29. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
  30. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon.png
  31. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon@2x.png
  32. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png
  33. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png
  34. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png
  35. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png
  36. binární
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/ios-marketing.png
  37. 6 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/Contents.json
  38. 27 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Base.lproj/LaunchScreen.storyboard
  39. 42 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Base.lproj/Main.storyboard
  40. 59 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Info.plist
  41. 14 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/TunneledWebView-Bridging-Header.h
  42. 33 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/ViewController.swift
  43. 15 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/psiphon-config.json.stub
  44. 2 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/psiphon-embedded-server-entries.txt.stub
  45. 22 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebViewTests/Info.plist
  46. 36 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebViewTests/TunneledWebViewTests.swift
  47. 22 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebViewUITests/Info.plist
  48. 36 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebViewUITests/TunneledWebViewUITests.swift

+ 217 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.h

@@ -0,0 +1,217 @@
+/*
+ File: JAHPAuthenticatingHTTPProtocol.h
+ Abstract: An NSURLProtocol subclass that overrides the built-in HTTP/HTTPS protocol.
+ 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 Foundation;
+
+@protocol JAHPAuthenticatingHTTPProtocolDelegate;
+
+/*! An NSURLProtocol subclass that overrides the built-in HTTP/HTTPS protocol to intercept
+ *  authentication challenges for subsystems, ilke UIWebView, that don't otherwise allow it.
+ *  To use this class you should set up your delegate (+setDelegate:) and then call +start.
+ *  If you don't call +start the class is completely benign.
+ *
+ *  The really tricky stuff here is related to the authentication challenge delegate
+ *  callbacks; see the docs for JAHPAuthenticatingHTTPProtocolDelegate for the details.
+ */
+
+@interface JAHPAuthenticatingHTTPProtocol : NSURLProtocol
+
+/*! Call this to start the module.  Prior to this the module is just dormant, and
+ *  all HTTP requests proceed as normal.  After this all HTTP and HTTPS requests
+ *  go through this module.
+ */
+
++ (void)start;
+
+/*! 
+ Unregisters the protocol
+ */
++ (void)stop;
+
+/*! Sets the delegate for the class.
+ *  \details Note that there's one delegate for the entire class, not one per
+ *  instance of the class as is more normal.  The delegate is not retained in general,
+ *  but is retained for the duration of any given call.  Once you set the delegate to nil
+ *  you can be assured that it won't be called unretained (that is, by the time that
+ *  -setDelegate: returns, we've already done all possible retains on the delegate).
+ *
+ *  The delegate is weakly referenced, so there's no risk of a crash if you don't
+ *  explicitly nil it.
+ *  \param newValue The new delegate to use; may be nil.
+ */
+
++ (void)setDelegate:(nullable id<JAHPAuthenticatingHTTPProtocolDelegate>)newValue;
+
+/*! Returns the class delegate.
+ */
+
++ (nullable id<JAHPAuthenticatingHTTPProtocolDelegate>)delegate;
+
+/*! Sets the user agent token
+ *  \details This token is appended to the system default user agent.
+ */
++ (void)setUserAgentToken:(nullable NSString *)userAgentToken;
+
+/*! Returns the user agent token.
+ */
++ (nullable NSString *)userAgentToken;
+
++ (void)resetSharedDemux;
+
+@property (atomic, strong, readonly)  NSURLAuthenticationChallenge * __nullable     pendingChallenge;   ///< The current authentication challenge; it's only safe to access this from the main thread.
+
+/*! Call this method to resolve an authentication challeng.  This must be called on the main thread.
+ *  \param challenge The challenge to resolve. This must match the pendingChallenge property.
+ *  \param credential The credential to use, or nil to continue without a credential.
+ */
+
+- (void)resolvePendingAuthenticationChallengeWithCredential:(nonnull NSURLCredential *)credential;
+- (void)cancelPendingAuthenticationChallenge;
+
+@end
+
+/*! The delegate for the JAHPAuthenticatingHTTPProtocol class (not its instances).
+ *  \details The delegate handles two different types of callbacks:
+ *
+ *  - authentication challenges
+ *
+ *  - logging
+ *
+ *  The latter is very simple.  The former is quite tricky.  The basic idea is that each JAHPAuthenticatingHTTPProtocol
+ *  instance sends the delegate a serialised stream of authentication challenges, each of which it is
+ *  expected to resolve.  The sequence is as follows:
+ *
+ *  -# It calls -authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace: to determine if the delegate
+ *     can handle the challenge.  This can be call on an arbitrary background thread.
+ *
+ *  -# If the delegate returns YES, it calls -authenticatingHTTPProtocol:didReceiveAuthenticationChallenge: to
+ *     actually process the challenge.  This is always called on the main thread.  The delegate can resolve
+ *     the challenge synchronously (that is, before returning from the call) or it can return from the call
+ *     and then, later on, resolve the challenge.  Resolving the challenge involves calling
+ *     -[JAHPAuthenticatingHTTPProtocol resolveAuthenticationChallenge:withCredential:], which also must be called
+ *     on the main thread.  Between the calls to -authenticatingHTTPProtocol:didReceiveAuthenticationChallenge:
+ *     and -[JAHPAuthenticatingHTTPProtocol resolveAuthenticationChallenge:withCredential:], the protocol's
+ *     pendingChallenge property will contain the challenge.
+ *
+ *  -# While there is a pending challenge, the protocol may call -authenticatingHTTPProtocol:didCancelAuthenticationChallenge:
+ *     to cancel the challenge.  This is always called on the main thread.
+ *
+ *  Note that this design follows the original NSURLConnection model, not the newer NSURLConnection model
+ *  (introduced in OS X 10.7 / iOS 5) or the NSURLSession model, because of my concerns about performance.
+ *  Specifically, -authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace: can be called on any thread
+ *  but -authenticatingHTTPProtocol:didReceiveAuthenticationChallenge: is called on the main thread.  If I unified
+ *  them I'd end up calling the resulting single routine on the main thread, which meanings a lot more
+ *  bouncing between threads, much of which would be pointless in the common case where you don't want to
+ *  customise the default behaviour.  Alternatively I could call the unified routine on an arbitrary thread,
+ *  but that would make it harder for clients and require a major rework of my implementation.
+ */
+
+typedef void (^JAHPDidCancelAuthenticationChallengeHandler)(JAHPAuthenticatingHTTPProtocol * __nonnull authenticatingHTTPProtocol, NSURLAuthenticationChallenge * __nonnull challenge);
+
+@protocol JAHPAuthenticatingHTTPProtocolDelegate <NSObject>
+
+@optional
+
+/*! Called by an JAHPAuthenticatingHTTPProtocol instance to ask the delegate whether it's prepared to handle
+ *  a particular authentication challenge.  Can be called on any thread.
+ *  \param protocol The protocol instance itself; will not be nil.
+ *  \param protectionSpace The protection space for the authentication challenge; will not be nil.
+ *  \returns Return YES if you want the -authenticatingHTTPProtocol:didReceiveAuthenticationChallenge: delegate
+ *  callback, or NO for the challenge to be handled in the default way.
+ */
+
+- (BOOL)authenticatingHTTPProtocol:(nonnull JAHPAuthenticatingHTTPProtocol *)authenticatingHTTPProtocol canAuthenticateAgainstProtectionSpace:(nonnull NSURLProtectionSpace *)protectionSpace;
+
+/*! Called by an JAHPAuthenticatingHTTPProtocol instance to request that the delegate process on authentication
+ *  challenge. Will be called on the main thread. Unless the challenge is cancelled (see below)
+ *  the delegate must eventually resolve it by calling -resolveAuthenticationChallenge:withCredential:.
+ *  \param protocol The protocol instance itself; will not be nil.
+ *  \param challenge The authentication challenge; will not be nil.
+ *  \returns an optional JAHPDidCancelAuthenticationChallengeHandler that will be called when the
+ *  JAHPAuthenticatingHTTPProtocol instance cancels the authentication challenge. Just like
+ *  -authenticatingHTTPProtocol:didCancelAuthenticationChallenge:. If this is returned, there is usually no need 
+ *  to implement -authenticatingHTTPProtocol:didCancelAuthenticationChallenge:. This block will be called before
+ *  -authenticatingHTTPProtocol:didCancelAuthenticationChallenge:.
+ */
+
+- (nullable JAHPDidCancelAuthenticationChallengeHandler)authenticatingHTTPProtocol:(nonnull JAHPAuthenticatingHTTPProtocol *)authenticatingHTTPProtocol didReceiveAuthenticationChallenge:(nonnull NSURLAuthenticationChallenge *)challenge;
+
+/*! Called by an JAHPAuthenticatingHTTPProtocol instance to cancel an issued authentication challenge.
+ *  Will be called on the main thread.
+ *  \param protocol The protocol instance itself; will not be nil.
+ *  \param challenge The authentication challenge; will not be nil; will match the challenge
+ *  previously issued by -authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace:.
+ */
+
+- (void)authenticatingHTTPProtocol:(nonnull JAHPAuthenticatingHTTPProtocol *)authenticatingHTTPProtocol didCancelAuthenticationChallenge:(nonnull NSURLAuthenticationChallenge *)challenge;
+
+/*! Called by the JAHPAuthenticatingHTTPProtocol to log various bits of information.
+ *  Can be called on any thread.
+ *  \param protocol The protocol instance itself; nil to indicate log messages from the class itself.
+ *  \param format A standard NSString-style format string; will not be nil.
+ *  \param arguments Arguments for that format string.
+ */
+
+- (void)authenticatingHTTPProtocol:(nullable JAHPAuthenticatingHTTPProtocol *)authenticatingHTTPProtocol logWithFormat:(nonnull NSString *)format
+// clang's static analyzer doesn't know that a va_list can't have an nullability annotation.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnullability-completeness"
+                         arguments:(va_list)arguments;
+#pragma clang diagnostic pop
+
+/*! Called by the JAHPAuthenticatingHTTPProtocol to log various bits of information. Use this if implementing in Swift. Swift doesn't like
+ * -authenticatingHTTPProtocol:logWithFormat:arguments: because 
+ * `Method cannot be marked @objc because the type of the parameter 3 cannot be represented in Objective-C`
+ *  I assume this is a problem with Swift not understanding that CVAListPointer should become va_list.
+ *  Can be called on any thread.
+ *  \param protocol The protocol instance itself; nil to indicate log messages from the class itself.
+ *  \param message A message to log
+ */
+
+- (void)authenticatingHTTPProtocol:(nullable JAHPAuthenticatingHTTPProtocol *)authenticatingHTTPProtocol logMessage:(nonnull NSString *)message;
+
+@end

+ 929 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m

@@ -0,0 +1,929 @@
+/*
+ File: JAHPAuthenticatingHTTPProtocol.m
+ Abstract: An NSURLProtocol subclass that overrides the built-in HTTP/HTTPS protocol.
+ 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 "JAHPAuthenticatingHTTPProtocol.h"
+
+#import "JAHPCanonicalRequest.h"
+#import "JAHPCacheStoragePolicy.h"
+#import "JAHPQNSURLSessionDemux.h"
+
+#import "TunneledWebView-Swift.h"
+
+// I use the following typedef to keep myself sane in the face of the wacky
+// Objective-C block syntax.
+
+typedef void (^JAHPChallengeCompletionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * credential);
+
+@interface JAHPWeakDelegateHolder : NSObject
+
+@property (nonatomic, weak) id<JAHPAuthenticatingHTTPProtocolDelegate> delegate;
+
+@end
+
+@interface JAHPAuthenticatingHTTPProtocol () <NSURLSessionDataDelegate>
+
+@property (atomic, strong, readwrite) NSThread *                        clientThread;       ///< The thread on which we should call the client.
+
+/*! The run loop modes in which to call the client.
+ *  \details The concurrency control here is complex.  It's set up on the client
+ *  thread in -startLoading and then never modified.  It is, however, read by code
+ *  running on other threads (specifically the main thread), so we deallocate it in
+ *  -dealloc rather than in -stopLoading.  We can be sure that it's not read before
+ *  it's set up because the main thread code that reads it can only be called after
+ *  -startLoading has started the connection running.
+ */
+
+@property (atomic, copy,   readwrite) NSArray *                         modes;
+@property (atomic, assign, readwrite) NSTimeInterval                    startTime;          ///< The start time of the request; written by client thread only; read by any thread.
+@property (atomic, strong, readwrite) NSURLSessionDataTask *            task;               ///< The NSURLSession task for that request; client thread only.
+@property (atomic, strong, readwrite) NSURLAuthenticationChallenge *    pendingChallenge;
+@property (atomic, copy,   readwrite) JAHPChallengeCompletionHandler        pendingChallengeCompletionHandler;  ///< The completion handler that matches pendingChallenge; main thread only.
+@property (atomic, copy,   readwrite) JAHPDidCancelAuthenticationChallengeHandler pendingDidCancelAuthenticationChallengeHandler;  ///< The handler that runs when we cancel the pendingChallenge; main thread only.
+
+@end
+
+@implementation JAHPAuthenticatingHTTPProtocol
+
+#pragma mark * Subclass specific additions
+
+/*! The backing store for the class delegate.  This is protected by @synchronized on the class.
+ */
+
+static JAHPWeakDelegateHolder* weakDelegateHolder;
+
+
+/*! A token to append to all HTTP user agent headers.
+ */
+static NSString * sUserAgentToken;
+
++ (void)start
+{
+    [NSURLProtocol registerClass:self];
+}
+
++ (void)stop {
+    [NSURLProtocol unregisterClass:self];
+}
+
++ (id<JAHPAuthenticatingHTTPProtocolDelegate>)delegate
+{
+    id<JAHPAuthenticatingHTTPProtocolDelegate> result;
+    
+    @synchronized (self) {
+        if (!weakDelegateHolder) {
+            weakDelegateHolder = [JAHPWeakDelegateHolder new];
+        }
+        result = weakDelegateHolder.delegate;
+    }
+    return result;
+}
+
++ (void)setDelegate:(id<JAHPAuthenticatingHTTPProtocolDelegate>)newValue
+{
+    @synchronized (self) {
+        if (!weakDelegateHolder) {
+            weakDelegateHolder = [JAHPWeakDelegateHolder new];
+        }
+        weakDelegateHolder.delegate = newValue;
+    }
+}
+
++ (NSString *)userAgentToken {
+    NSString *userAgentToken;
+    @synchronized(self) {
+        userAgentToken = sUserAgentToken;
+    }
+    return userAgentToken;
+}
+
++ (void)setUserAgentToken:(NSString *)userAgentToken {
+    @synchronized(self) {
+        sUserAgentToken = userAgentToken;
+    }
+}
+
+/*! Returns the session demux object used by all the protocol instances.
+ *  \details This object allows us to have a single NSURLSession, with a session delegate,
+ *  and have its delegate callbacks routed to the correct protocol instance on the correct
+ *  thread in the correct modes.  Can be called on any thread.
+ */
+
+static JAHPQNSURLSessionDemux *sharedDemuxInstance = nil;
+
++ (JAHPQNSURLSessionDemux *)sharedDemux
+{
+    static dispatch_once_t      sOnceToken;
+    static JAHPQNSURLSessionDemux * sharedDemuxInstance;
+    dispatch_once(&sOnceToken, ^{
+        NSURLSessionConfiguration *     config;
+        
+        config = [NSURLSessionConfiguration defaultSessionConfiguration];
+        // You have to explicitly configure the session to use your own protocol subclass here
+        // otherwise you don't see redirects <rdar://problem/17384498>.
+        if (config.protocolClasses) {
+            config.protocolClasses = [config.protocolClasses arrayByAddingObject:self];
+        } else {
+            config.protocolClasses = @[ self ];
+        }
+        
+        // Set proxy
+        NSString* proxyHost = @"localhost";
+        NSNumber* socksProxyPort = [NSNumber numberWithInt: (int)[AppDelegate sharedDelegate].socksProxyPort];
+        NSNumber* httpProxyPort = [NSNumber numberWithInt: (int)[AppDelegate sharedDelegate].httpProxyPort];
+
+        NSDictionary *proxyDict = @{
+                                    @"SOCKSEnable" : [NSNumber numberWithInt:0],
+                                    (NSString *)kCFStreamPropertySOCKSProxyHost : proxyHost,
+                                    (NSString *)kCFStreamPropertySOCKSProxyPort : socksProxyPort,
+
+                                    @"HTTPEnable"  : [NSNumber numberWithInt:1],
+                                    (NSString *)kCFStreamPropertyHTTPProxyHost  : proxyHost,
+                                    (NSString *)kCFStreamPropertyHTTPProxyPort  : httpProxyPort,
+
+                                    @"HTTPSEnable" : [NSNumber numberWithInt:1],
+                                    (NSString *)kCFStreamPropertyHTTPSProxyHost : proxyHost,
+                                    (NSString *)kCFStreamPropertyHTTPSProxyPort : httpProxyPort,
+                                    };
+        config.connectionProxyDictionary = proxyDict;
+
+        sharedDemuxInstance = [[JAHPQNSURLSessionDemux alloc] initWithConfiguration:config];
+    });
+    return sharedDemuxInstance;
+}
+
++ (void)resetSharedDemux
+{
+    @synchronized(self) {
+        sharedDemuxInstance = nil;
+    }
+}
+
+/*! Called by by both class code and instance code to log various bits of information.
+ *  Can be called on any thread.
+ *  \param protocol The protocol instance; nil if it's the class doing the logging.
+ *  \param format A standard NSString-style format string; will not be nil.
+ */
+
++ (void)authenticatingHTTPProtocol:(JAHPAuthenticatingHTTPProtocol *)protocol logWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(2, 3)
+// All internal logging calls this routine, which routes the log message to the
+// delegate.
+{
+    // protocol may be nil
+    id<JAHPAuthenticatingHTTPProtocolDelegate> strongDelegate;
+    
+    strongDelegate = [self delegate];
+    if ([strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:logWithFormat:arguments:)]) {
+        va_list arguments;
+        
+        va_start(arguments, format);
+        [strongDelegate authenticatingHTTPProtocol:protocol logWithFormat:format arguments:arguments];
+        va_end(arguments);
+    }
+    if ([strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:logMessage:)]) {
+        va_list arguments;
+        
+        va_start(arguments, format);
+        NSString *message = [[NSString alloc] initWithFormat:format arguments:arguments];
+        va_end(arguments);
+        [strongDelegate authenticatingHTTPProtocol:protocol logMessage:message];
+    }
+}
+
+#pragma mark * NSURLProtocol overrides
+
+/*! Used to mark our recursive requests so that we don't try to handle them (and thereby
+ *  suffer an infinite recursive death).
+ */
+
+static NSString * kJAHPRecursiveRequestFlagProperty = @"com.jivesoftware.JAHPAuthenticatingHTTPProtocol";
+
++ (BOOL)canInitWithRequest:(NSURLRequest *)request
+{
+    BOOL        shouldAccept;
+    NSURL *     url;
+    NSString *  scheme;
+    
+    // Check the basics.  This routine is extremely defensive because experience has shown that
+    // it can be called with some very odd requests <rdar://problem/15197355>.
+    
+    shouldAccept = (request != nil);
+    if (shouldAccept) {
+        url = [request URL];
+        shouldAccept = (url != nil);
+    }
+    if ( ! shouldAccept ) {
+        [self authenticatingHTTPProtocol:nil logWithFormat:@"decline request (malformed)"];
+    }
+    
+    // Decline our recursive requests.
+    
+    if (shouldAccept) {
+        shouldAccept = ([self propertyForKey:kJAHPRecursiveRequestFlagProperty inRequest:request] == nil);
+        if ( ! shouldAccept ) {
+            [self authenticatingHTTPProtocol:nil logWithFormat:@"decline request %@ (recursive)", url];
+        }
+    }
+    
+    // Get the scheme.
+    
+    if (shouldAccept) {
+        scheme = [[url scheme] lowercaseString];
+        shouldAccept = (scheme != nil);
+        
+        if ( ! shouldAccept ) {
+            [self authenticatingHTTPProtocol:nil logWithFormat:@"decline request %@ (no scheme)", url];
+        }
+    }
+    
+    // Look for "http" or "https".
+    //
+    // Flip either or both of the following to YESes to control which schemes go through this custom
+    // NSURLProtocol subclass.
+    
+    if (shouldAccept) {
+        shouldAccept = YES && [scheme isEqual:@"http"];
+        if ( ! shouldAccept ) {
+            shouldAccept = YES && [scheme isEqual:@"https"];
+        }
+        
+        if ( ! shouldAccept ) {
+            [self authenticatingHTTPProtocol:nil logWithFormat:@"decline request %@ (scheme mismatch)", url];
+        } else {
+            [self authenticatingHTTPProtocol:nil logWithFormat:@"accept request %@", url];
+        }
+    }
+    
+    return shouldAccept;
+}
+
++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
+{
+    NSURLRequest *      result;
+    
+    assert(request != nil);
+    // can be called on any thread
+    
+    // Canonicalising a request is quite complex, so all the heavy lifting has
+    // been shuffled off to a separate module.
+    
+    result = JAHPCanonicalRequestForRequest(request);
+    
+    [self authenticatingHTTPProtocol:nil logWithFormat:@"canonicalized %@ to %@", [request URL], [result URL]];
+    
+    return result;
+}
+
+- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client
+{
+    assert(request != nil);
+    // cachedResponse may be nil
+    assert(client != nil);
+    // can be called on any thread
+    
+    NSMutableURLRequest *mutableRequest = [request mutableCopy];
+    NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:mutableRequest.URL];
+    NSString *cookieString = @"";
+    for (NSHTTPCookie *cookie in cookies) {
+        cookieString = [cookieString stringByAppendingString:[NSString stringWithFormat:@"%@=%@; ", cookie.name, cookie.value]];
+    }
+    if ([cookieString length] > 0) {
+        cookieString = [cookieString substringToIndex:[cookieString length] - 2];
+        NSUInteger cookieStringBytes = [cookieString lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+        if (cookieStringBytes > 3999) {
+            [mutableRequest setValue:cookieString forHTTPHeaderField:@"Cookie"];
+            
+        }
+    }
+    
+    NSString *userAgentToken = [[self class] userAgentToken];
+    if ([userAgentToken length]) {
+        // use addValue:forHTTPHeaderField: instead of setValue:forHTTPHeaderField:.
+        // we want to append the userAgentToken to the existing user agent instead of
+        // replacing the existing user agent.
+        [mutableRequest addValue:userAgentToken forHTTPHeaderField:@"User-Agent"];
+    }
+    
+    self = [super initWithRequest:mutableRequest cachedResponse:cachedResponse client:client];
+    if (self != nil) {
+        // All we do here is log the call.
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"init for %@ from <%@ %p>", [request URL], [client class], client];
+    }
+    return self;
+}
+
+- (void)dealloc
+{
+    // can be called on any thread
+    [[self class] authenticatingHTTPProtocol:self logWithFormat:@"dealloc"];
+    assert(self->_task == nil);                     // we should have cleared it by now
+    assert(self->_pendingChallenge == nil);         // we should have cancelled it by now
+    assert(self->_pendingChallengeCompletionHandler == nil);    // we should have cancelled it by now
+}
+
+- (void)startLoading
+{
+    NSMutableURLRequest *   recursiveRequest;
+    NSMutableArray *        calculatedModes;
+    NSString *              currentMode;
+    
+    // At this point we kick off the process of loading the URL via NSURLSession.
+    // The thread that calls this method becomes the client thread.
+    
+    assert(self.clientThread == nil);           // you can't call -startLoading twice
+    assert(self.task == nil);
+    
+    // Calculate our effective run loop modes.  In some circumstances (yes I'm looking at
+    // you UIWebView!) we can be called from a non-standard thread which then runs a
+    // non-standard run loop mode waiting for the request to finish.  We detect this
+    // non-standard mode and add it to the list of run loop modes we use when scheduling
+    // our callbacks.  Exciting huh?
+    //
+    // For debugging purposes the non-standard mode is "WebCoreSynchronousLoaderRunLoopMode"
+    // but it's better not to hard-code that here.
+    
+    assert(self.modes == nil);
+    calculatedModes = [NSMutableArray array];
+    [calculatedModes addObject:NSDefaultRunLoopMode];
+    currentMode = [[NSRunLoop currentRunLoop] currentMode];
+    if ( (currentMode != nil) && ! [currentMode isEqual:NSDefaultRunLoopMode] ) {
+        [calculatedModes addObject:currentMode];
+    }
+    self.modes = calculatedModes;
+    assert([self.modes count] > 0);
+    
+    // Create new request that's a clone of the request we were initialised with,
+    // except that it has our 'recursive request flag' property set on it.
+    
+    recursiveRequest = [[self request] mutableCopy];
+    assert(recursiveRequest != nil);
+    
+    [[self class] setProperty:@YES forKey:kJAHPRecursiveRequestFlagProperty inRequest:recursiveRequest];
+    
+    self.startTime = [NSDate timeIntervalSinceReferenceDate];
+    if (currentMode == nil) {
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"start %@", [recursiveRequest URL]];
+    } else {
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"start %@ (mode %@)", [recursiveRequest URL], currentMode];
+    }
+    
+    // Latch the thread we were called on, primarily for debugging purposes.
+    
+    self.clientThread = [NSThread currentThread];
+    
+    // Once everything is ready to go, create a data task with the new request.
+    
+    self.task = [[[self class] sharedDemux] dataTaskWithRequest:recursiveRequest delegate:self modes:self.modes];
+    assert(self.task != nil);
+    
+    [self.task resume];
+}
+
+- (void)stopLoading
+{
+    // The implementation just cancels the current load (if it's still running).
+    
+    [[self class] authenticatingHTTPProtocol:self logWithFormat:@"stop (elapsed %.1f)", [NSDate timeIntervalSinceReferenceDate] - self.startTime];
+    
+    assert(self.clientThread != nil);           // someone must have called -startLoading
+    
+    // Check that we're being stopped on the same thread that we were started
+    // on.  Without this invariant things are going to go badly (for example,
+    // run loop sources that got attached during -startLoading may not get
+    // detached here).
+    //
+    // I originally had code here to bounce over to the client thread but that
+    // actually gets complex when you consider run loop modes, so I've nixed it.
+    // Rather, I rely on our client calling us on the right thread, which is what
+    // the following assert is about.
+    
+    assert([NSThread currentThread] == self.clientThread);
+    
+    [self cancelPendingChallenge];
+    if (self.task != nil) {
+        [self.task cancel];
+        self.task = nil;
+        // The following ends up calling -URLSession:task:didCompleteWithError: with NSURLErrorDomain / NSURLErrorCancelled,
+        // which specificallys traps and ignores the error.
+    }
+    // Don't nil out self.modes; see property declaration comments for a a discussion of this.
+}
+
+#pragma mark * Authentication challenge handling
+
+/*! Performs the block on the specified thread in one of specified modes.
+ *  \param thread The thread to target; nil implies the main thread.
+ *  \param modes The modes to target; nil or an empty array gets you the default run loop mode.
+ *  \param block The block to run.
+ */
+
+- (void)performOnThread:(NSThread *)thread modes:(NSArray *)modes block:(dispatch_block_t)block
+{
+    // thread may be nil
+    // modes may be nil
+    assert(block != nil);
+    
+    if (thread == nil) {
+        thread = [NSThread mainThread];
+    }
+    if ([modes count] == 0) {
+        modes = @[ NSDefaultRunLoopMode ];
+    }
+    [self performSelector:@selector(onThreadPerformBlock:) onThread:thread withObject:[block copy] waitUntilDone:NO modes:modes];
+}
+
+/*! A helper method used by -performOnThread:modes:block:. Runs in the specified context
+ *  and simply calls the block.
+ *  \param block The block to run.
+ */
+
+- (void)onThreadPerformBlock:(dispatch_block_t)block
+{
+    assert(block != nil);
+    block();
+}
+
+/*! Called by our NSURLSession delegate callback to pass the challenge to our delegate.
+ *  \description This simply passes the challenge over to the main thread.
+ *  We do this so that all accesses to pendingChallenge are done from the main thread,
+ *  which avoids the need for extra synchronisation.
+ *
+ *  By the time this runes, the NSURLSession delegate callback has already confirmed with
+ *  the delegate that it wants the challenge.
+ *
+ *  Note that we use the default run loop mode here, not the common modes.  We don't want
+ *  an authorisation dialog showing up on top of an active menu (-:
+ *
+ *  Also, we implement our own 'perform block' infrastructure because Cocoa doesn't have
+ *  one <rdar://problem/17232344> and CFRunLoopPerformBlock is inadequate for the
+ *  return case (where we need to pass in an array of modes; CFRunLoopPerformBlock only takes
+ *  one mode).
+ *  \param challenge The authentication challenge to process; must not be nil.
+ *  \param completionHandler The associated completion handler; must not be nil.
+ */
+
+- (void)didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(JAHPChallengeCompletionHandler)completionHandler
+{
+    assert(challenge != nil);
+    assert(completionHandler != nil);
+    assert([NSThread currentThread] == self.clientThread);
+    
+    [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ received", [[challenge protectionSpace] authenticationMethod]];
+    
+    [self performOnThread:nil modes:nil block:^{
+        [self mainThreadDidReceiveAuthenticationChallenge:challenge completionHandler:completionHandler];
+    }];
+}
+
+/*! The main thread side of authentication challenge processing.
+ *  \details If there's already a pending challenge, something has gone wrong and
+ *  the routine simply cancels the new challenge.  If our delegate doesn't implement
+ *  the -authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace: delegate callback,
+ *  we also cancel the challenge.  OTOH, if all goes well we simply call our delegate
+ *  with the challenge.
+ *  \param challenge The authentication challenge to process; must not be nil.
+ *  \param completionHandler The associated completion handler; must not be nil.
+ */
+
+- (void)mainThreadDidReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(JAHPChallengeCompletionHandler)completionHandler
+{
+    assert(challenge != nil);
+    assert(completionHandler != nil);
+    assert([NSThread isMainThread]);
+    
+    if (self.pendingChallenge != nil) {
+        
+        // Our delegate is not expecting a second authentication challenge before resolving the
+        // first.  Likewise, NSURLSession shouldn't send us a second authentication challenge
+        // before we resolve the first.  If this happens, assert, log, and cancel the challenge.
+        //
+        // Note that we have to cancel the challenge on the thread on which we received it,
+        // namely, the client thread.
+        
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ cancelled; other challenge pending", [[challenge protectionSpace] authenticationMethod]];
+        assert(NO);
+        [self clientThreadCancelAuthenticationChallenge:challenge completionHandler:completionHandler];
+    } else {
+        id<JAHPAuthenticatingHTTPProtocolDelegate>  strongDelegate;
+        
+        strongDelegate = [[self class] delegate];
+        
+        // Tell the delegate about it.  It would be weird if the delegate didn't support this
+        // selector (it did return YES from -authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace:
+        // after all), but if it doesn't then we just cancel the challenge ourselves (or the client
+        // thread, of course).
+        
+        if ( ! [strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace:)] ) {
+            [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ cancelled; no delegate method", [[challenge protectionSpace] authenticationMethod]];
+            assert(NO);
+            [self clientThreadCancelAuthenticationChallenge:challenge completionHandler:completionHandler];
+        } else {
+            
+            // Remember that this challenge is in progress.
+            
+            self.pendingChallenge = challenge;
+            self.pendingChallengeCompletionHandler = completionHandler;
+            
+            // Pass the challenge to the delegate.
+            
+            [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ passed to delegate", [[challenge protectionSpace] authenticationMethod]];
+            self.pendingDidCancelAuthenticationChallengeHandler = [strongDelegate authenticatingHTTPProtocol:self didReceiveAuthenticationChallenge:self.pendingChallenge];
+        }
+    }
+}
+
+/*! Cancels an authentication challenge that hasn't made it to the pending challenge state.
+ *  \details This routine is called as part of various error cases in the challenge handling
+ *  code.  It cancels a challenge that, for some reason, we've failed to pass to our delegate.
+ *
+ *  The routine is always called on the main thread but bounces over to the client thread to
+ *  do the actual cancellation.
+ *  \param challenge The authentication challenge to cancel; must not be nil.
+ *  \param completionHandler The associated completion handler; must not be nil.
+ */
+
+- (void)clientThreadCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(JAHPChallengeCompletionHandler)completionHandler
+{
+#pragma unused(challenge)
+    assert(challenge != nil);
+    assert(completionHandler != nil);
+    assert([NSThread isMainThread]);
+    
+    [self performOnThread:self.clientThread modes:self.modes block:^{
+        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+    }];
+}
+
+/*! Cancels an authentication challenge that /has/ made to the pending challenge state.
+ *  \details This routine is called by -stopLoading to cancel any challenge that might be
+ *  pending when the load is cancelled.  It's always called on the client thread but
+ *  immediately bounces over to the main thread (because .pendingChallenge is a main
+ *  thread only value).
+ */
+
+- (void)cancelPendingChallenge
+{
+    assert([NSThread currentThread] == self.clientThread);
+    
+    // Just pass the work off to the main thread.  We do this so that all accesses
+    // to pendingChallenge are done from the main thread, which avoids the need for
+    // extra synchronisation.
+    
+    [self performOnThread:nil modes:nil block:^{
+        if (self.pendingChallenge == nil) {
+            // This is not only not unusual, it's actually very typical.  It happens every time you shut down
+            // the connection.  Ideally I'd like to not even call -mainThreadCancelPendingChallenge when
+            // there's no challenge outstanding, but the synchronisation issues are tricky.  Rather than solve
+            // those, I'm just not going to log in this case.
+            //
+            // [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge not cancelled; no challenge pending"];
+        } else {
+            id<JAHPAuthenticatingHTTPProtocolDelegate>  strongeDelegate;
+            NSURLAuthenticationChallenge *  challenge;
+            JAHPDidCancelAuthenticationChallengeHandler  didCancelAuthenticationChallengeHandler;
+            
+            strongeDelegate = [[self class] delegate];
+            
+            challenge = self.pendingChallenge;
+            didCancelAuthenticationChallengeHandler = self.pendingDidCancelAuthenticationChallengeHandler;
+            self.pendingChallenge = nil;
+            self.pendingChallengeCompletionHandler = nil;
+            self.pendingDidCancelAuthenticationChallengeHandler = nil;
+            
+            if ([strongeDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:didCancelAuthenticationChallenge:)]) {
+                [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ cancellation passed to delegate", [[challenge protectionSpace] authenticationMethod]];
+                if (didCancelAuthenticationChallengeHandler) {
+                    didCancelAuthenticationChallengeHandler(self, challenge);
+                }
+                [strongeDelegate authenticatingHTTPProtocol:self didCancelAuthenticationChallenge:challenge];
+            } else if (didCancelAuthenticationChallengeHandler) {
+                didCancelAuthenticationChallengeHandler(self, challenge);
+            } else {
+                [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ cancellation failed; no delegate method", [[challenge protectionSpace] authenticationMethod]];
+                // If we managed to send a challenge to the client but can't cancel it, that's bad.
+                // There's nothing we can do at this point except log the problem.
+                assert(NO);
+            }
+        }
+    }];
+}
+
+- (void)resolvePendingAuthenticationChallengeWithCredential:(NSURLCredential *)credential
+{
+    // credential may be nil
+    assert([NSThread isMainThread]);
+    assert(self.clientThread != nil);
+    
+    JAHPChallengeCompletionHandler  completionHandler;
+    NSURLAuthenticationChallenge *challenge;
+    
+    // We clear out our record of the pending challenge and then pass the real work
+    // over to the client thread (which ensures that the challenge is resolved on
+    // the same thread we received it on).
+    
+    completionHandler = self.pendingChallengeCompletionHandler;
+    challenge = self.pendingChallenge;
+    self.pendingChallenge = nil;
+    self.pendingChallengeCompletionHandler = nil;
+    self.pendingDidCancelAuthenticationChallengeHandler = nil;
+    
+    [self performOnThread:self.clientThread modes:self.modes block:^{
+        if (credential == nil) {
+            [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ resolved without credential", [[challenge protectionSpace] authenticationMethod]];
+            completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+        } else {
+            [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ resolved with <%@ %p>", [[challenge protectionSpace] authenticationMethod], [credential class], credential];
+            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
+        }
+    }];
+}
+
+- (void)cancelPendingAuthenticationChallenge {
+    assert([NSThread isMainThread]);
+    assert(self.clientThread != nil);
+    
+    JAHPChallengeCompletionHandler  completionHandler;
+    NSURLAuthenticationChallenge *challenge;
+    
+    // We clear out our record of the pending challenge and then pass the real work
+    // over to the client thread (which ensures that the challenge is resolved on
+    // the same thread we received it on).
+    
+    completionHandler = self.pendingChallengeCompletionHandler;
+    challenge = self.pendingChallenge;
+    self.pendingChallenge = nil;
+    self.pendingChallengeCompletionHandler = nil;
+    self.pendingDidCancelAuthenticationChallengeHandler = nil;
+    
+    [self performOnThread:self.clientThread modes:self.modes block:^{
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ was canceled", [[challenge protectionSpace] authenticationMethod]];
+        
+        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
+    }];
+}
+
+
+#pragma mark * NSURLSession delegate callbacks
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
+{
+    // rdar://21484589
+    // this is called from JAHPQNSURLSessionDemuxTaskInfo,
+    // which is called from the NSURLSession delegateQueue,
+    // which is a different thread than self.clientThread.
+    // It is possible that -stopLoading was called on self.clientThread
+    // just before this method if so, ignore this callback
+    if (!self.task) { return; }
+        
+    NSMutableURLRequest *    redirectRequest;
+    
+#pragma unused(session)
+#pragma unused(task)
+    assert(task == self.task);
+    assert(response != nil);
+    assert(newRequest != nil);
+#pragma unused(completionHandler)
+    assert(completionHandler != nil);
+    assert([NSThread currentThread] == self.clientThread);
+    
+    [[self class] authenticatingHTTPProtocol:self logWithFormat:@"will redirect from %@ to %@", [response URL], [newRequest URL]];
+    
+    // The new request was copied from our old request, so it has our magic property.  We actually
+    // have to remove that so that, when the client starts the new request, we see it.  If we
+    // don't do this then we never see the new request and thus don't get a chance to change
+    // its caching behaviour.
+    //
+    // We also cancel our current connection because the client is going to start a new request for
+    // us anyway.
+    
+    assert([[self class] propertyForKey:kJAHPRecursiveRequestFlagProperty inRequest:newRequest] != nil);
+    
+    redirectRequest = [newRequest mutableCopy];
+    [[self class] removePropertyForKey:kJAHPRecursiveRequestFlagProperty inRequest:redirectRequest];
+    
+    // Tell the client about the redirect.
+    
+    [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response];
+    
+    // Stop our load.  The CFNetwork infrastructure will create a new NSURLProtocol instance to run
+    // the load of the redirect.
+    
+    // The following ends up calling -URLSession:task:didCompleteWithError: with NSURLErrorDomain / NSURLErrorCancelled,
+    // which specificallys traps and ignores the error.
+    
+    [self.task cancel];
+    
+    [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
+}
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
+{
+    // rdar://21484589
+    // this is called from JAHPQNSURLSessionDemuxTaskInfo,
+    // which is called from the NSURLSession delegateQueue,
+    // which is a different thread than self.clientThread.
+    // It is possible that -stopLoading was called on self.clientThread
+    // just before this method if so, ignore this callback
+    if (!self.task) { return; }
+    
+    BOOL        result;
+    id<JAHPAuthenticatingHTTPProtocolDelegate> strongeDelegate;
+    
+#pragma unused(session)
+#pragma unused(task)
+    assert(task == self.task);
+    assert(challenge != nil);
+    assert(completionHandler != nil);
+    assert([NSThread currentThread] == self.clientThread);
+    
+    // Ask our delegate whether it wants this challenge.  We do this from this thread, not the main thread,
+    // to avoid the overload of bouncing to the main thread for challenges that aren't going to be customised
+    // anyway.
+    
+    strongeDelegate = [[self class] delegate];
+    
+    result = NO;
+    if ([strongeDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace:)]) {
+        result = [strongeDelegate authenticatingHTTPProtocol:self canAuthenticateAgainstProtectionSpace:[challenge protectionSpace]];
+    }
+    
+    // If the client wants the challenge, kick off that process.  If not, resolve it by doing the default thing.
+    
+    if (result) {
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"can authenticate %@", [[challenge protectionSpace] authenticationMethod]];
+        
+        [self didReceiveAuthenticationChallenge:challenge completionHandler:completionHandler];
+    } else {
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"cannot authenticate %@", [[challenge protectionSpace] authenticationMethod]];
+        
+        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
+{
+    // rdar://21484589
+    // this is called from JAHPQNSURLSessionDemuxTaskInfo,
+    // which is called from the NSURLSession delegateQueue,
+    // which is a different thread than self.clientThread.
+    // It is possible that -stopLoading was called on self.clientThread
+    // just before this method if so, ignore this callback
+    if (!self.task) { return; }
+    
+    NSURLCacheStoragePolicy cacheStoragePolicy;
+    NSInteger               statusCode;
+    
+#pragma unused(session)
+#pragma unused(dataTask)
+    assert(dataTask == self.task);
+    assert(response != nil);
+    assert(completionHandler != nil);
+    assert([NSThread currentThread] == self.clientThread);
+    
+    // Pass the call on to our client.  The only tricky thing is that we have to decide on a
+    // cache storage policy, which is based on the actual request we issued, not the request
+    // we were given.
+    
+    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+        cacheStoragePolicy = JAHPCacheStoragePolicyForRequestAndResponse(self.task.originalRequest, (NSHTTPURLResponse *) response);
+        statusCode = [((NSHTTPURLResponse *) response) statusCode];
+    } else {
+        assert(NO);
+        cacheStoragePolicy = NSURLCacheStorageNotAllowed;
+        statusCode = 42;
+    }
+    
+    [[self class] authenticatingHTTPProtocol:self logWithFormat:@"received response %zd / %@ with cache storage policy %zu", (ssize_t) statusCode, [response URL], (size_t) cacheStoragePolicy];
+    
+    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:cacheStoragePolicy];
+    
+    completionHandler(NSURLSessionResponseAllow);
+}
+
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
+{
+    // rdar://21484589
+    // this is called from JAHPQNSURLSessionDemuxTaskInfo,
+    // which is called from the NSURLSession delegateQueue,
+    // which is a different thread than self.clientThread.
+    // It is possible that -stopLoading was called on self.clientThread
+    // just before this method if so, ignore this callback
+    if (!self.task) { return; }
+    
+#pragma unused(session)
+#pragma unused(dataTask)
+    assert(dataTask == self.task);
+    assert(data != nil);
+    assert([NSThread currentThread] == self.clientThread);
+    
+    // Just pass the call on to our client.
+    
+    [[self class] authenticatingHTTPProtocol:self logWithFormat:@"received %zu bytes of data", (size_t) [data length]];
+    
+    [[self client] URLProtocol:self didLoadData:data];
+}
+
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *))completionHandler
+{
+    // rdar://21484589
+    // this is called from JAHPQNSURLSessionDemuxTaskInfo,
+    // which is called from the NSURLSession delegateQueue,
+    // which is a different thread than self.clientThread.
+    // It is possible that -stopLoading was called on self.clientThread
+    // just before this method if so, ignore this callback
+    if (!self.task) { return; }
+    
+#pragma unused(session)
+#pragma unused(dataTask)
+    assert(dataTask == self.task);
+    assert(proposedResponse != nil);
+    assert(completionHandler != nil);
+    assert([NSThread currentThread] == self.clientThread);
+    
+    // We implement this delegate callback purely for the purposes of logging.
+    
+    [[self class] authenticatingHTTPProtocol:self logWithFormat:@"will cache response"];
+    
+    completionHandler(proposedResponse);
+}
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
+// An NSURLSession delegate callback.  We pass this on to the client.
+{
+#pragma unused(session)
+#pragma unused(task)
+    assert( (self.task == nil) || (task == self.task) );        // can be nil in the 'cancel from -stopLoading' case
+    assert([NSThread currentThread] == self.clientThread);
+    
+    // Just log and then, in most cases, pass the call on to our client.
+    
+    if (error == nil) {
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"success"];
+        
+        [[self client] URLProtocolDidFinishLoading:self];
+    } else if ( [[error domain] isEqual:NSURLErrorDomain] && ([error code] == NSURLErrorCancelled) ) {
+        // Do nothing.  This happens in two cases:
+        //
+        // o during a redirect, in which case the redirect code has already told the client about
+        //   the failure
+        //
+        // o if the request is cancelled by a call to -stopLoading, in which case the client doesn't
+        //   want to know about the failure
+    } else {
+        [[self class] authenticatingHTTPProtocol:self logWithFormat:@"error %@ / %d", [error domain], (int) [error code]];
+        
+        [[self client] URLProtocol:self didFailWithError:error];
+    }
+    
+    // We don't need to clean up the connection here; the system will call, or has already called,
+    // -stopLoading to do that.
+}
+
+@end
+
+@implementation JAHPWeakDelegateHolder
+
+@end

+ 61 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCacheStoragePolicy.h

@@ -0,0 +1,61 @@
+/*
+ File: JAHPCacheStoragePolicy.h
+ Abstract: A function to determine the cache storage policy for a request.
+ 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 Foundation;
+
+/*! Determines the cache storage policy for a response.
+ *  \details When we provide a response up to the client we need to tell the client whether
+ *  the response is cacheable or not.  The default HTTP/HTTPS protocol has a reasonable
+ *  complex chunk of code to determine this, but we can't get at it.  Thus, we have to
+ *  reimplement it ourselves.  This is split off into a separate file to emphasise that
+ *  this is standard boilerplate that you probably don't need to look at.
+ *  \param request The request that generated the response; must not be nil.
+ *  \param response The response itself; must not be nil.
+ *  \returns A cache storage policy to use.
+ */
+
+extern NSURLCacheStoragePolicy JAHPCacheStoragePolicyForRequestAndResponse(NSURLRequest * request, NSHTTPURLResponse * response);

+ 124 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCacheStoragePolicy.m

@@ -0,0 +1,124 @@
+/*
+ File: JAHPCacheStoragePolicy.m
+ Abstract: A function to determine the cache storage policy for a request.
+ 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 "JAHPCacheStoragePolicy.h"
+
+extern NSURLCacheStoragePolicy JAHPCacheStoragePolicyForRequestAndResponse(NSURLRequest * request, NSHTTPURLResponse * response)
+// See comment in header.
+{
+    BOOL                        cacheable;
+    NSURLCacheStoragePolicy     result;
+    
+    assert(request != NULL);
+    assert(response != NULL);
+    
+    // First determine if the request is cacheable based on its status code.
+    
+    switch ([response statusCode]) {
+        case 200:
+        case 203:
+        case 206:
+        case 301:
+        case 304:
+        case 404:
+        case 410: {
+            cacheable = YES;
+        } break;
+        default: {
+            cacheable = NO;
+        } break;
+    }
+    
+    // If the response might be cacheable, look at the "Cache-Control" header in
+    // the response.
+    
+    // IMPORTANT: We can't rely on -rangeOfString: returning valid results if the target
+    // string is nil, so we have to explicitly test for nil in the following two cases.
+    
+    if (cacheable) {
+        NSString *  responseHeader;
+        
+        responseHeader = [[response allHeaderFields][@"Cache-Control"] lowercaseString];
+        if ( (responseHeader != nil) && [responseHeader rangeOfString:@"no-store"].location != NSNotFound) {
+            cacheable = NO;
+        }
+    }
+    
+    // If we still think it might be cacheable, look at the "Cache-Control" header in
+    // the request.
+    
+    if (cacheable) {
+        NSString *  requestHeader;
+        
+        requestHeader = [[request allHTTPHeaderFields][@"Cache-Control"] lowercaseString];
+        if ( (requestHeader != nil)
+            && ([requestHeader rangeOfString:@"no-store"].location != NSNotFound)
+            && ([requestHeader rangeOfString:@"no-cache"].location != NSNotFound) ) {
+            cacheable = NO;
+        }
+    }
+    
+    // Use the cacheable flag to determine the result.
+    
+    if (cacheable) {
+        
+        // This code only caches HTTPS data in memory.  This is inline with earlier versions of
+        // iOS.  Modern versions of iOS use file protection to protect the cache, and thus are
+        // happy to cache HTTPS on disk.  I've not made the correspondencing change because
+        // it's nice to see all three cache policies in action.
+        
+        if ([[[[request URL] scheme] lowercaseString] isEqual:@"https"]) {
+            result = NSURLCacheStorageAllowedInMemoryOnly;
+        } else {
+            result = NSURLCacheStorageAllowed;
+        }
+    } else {
+        result = NSURLCacheStorageNotAllowed;
+    }
+    
+    return result;
+}

+ 64 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCanonicalRequest.h

@@ -0,0 +1,64 @@
+/*
+ File: JAHPCanonicalRequest.h
+ Abstract: A function for creating canonical HTTP/HTTPS requests.
+ 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 Foundation;
+
+/*! Returns a canonical form of the supplied request.
+ *  \details The Foundation URL loading system needs to be able to canonicalize URL
+ *  requests for various reasons (for example, to look for cache hits).  The default
+ *  HTTP/HTTPS protocol has a complex chunk of code to perform this function.  Unfortunately
+ *  there's no way for third party code to access this.  Instead, we have to reimplement
+ *  it all ourselves.  This is split off into a separate file to emphasise that this
+ *  is standard boilerplate that you probably don't need to look at.
+ *
+ *  IMPORTANT: While you can take most of this code as read, you might want to tweak
+ *  the handling of the "Accept-Language" in the CanonicaliseHeaders routine.
+ *  \param request The request to canonicalise; must not be nil.
+ *  \returns The canonical request; should never be nil.
+ */
+
+extern NSMutableURLRequest * JAHPCanonicalRequestForRequest(NSURLRequest *request);

+ 459 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPCanonicalRequest.m

@@ -0,0 +1,459 @@
+/*
+ File: JAHPCanonicalRequest.m
+ Abstract: A function for creating canonical HTTP/HTTPS requests.
+ 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 "JAHPCanonicalRequest.h"
+
+#include <xlocale.h>
+
+#pragma mark * URL canonicalization steps
+
+/*! A step in the canonicalisation process.
+ *  \details The canonicalisation process is made up of a sequence of steps, each of which is
+ *  implemented by a function that matches this function pointer.  The function gets a URL
+ *  and a mutable buffer holding that URL as bytes.  The function can mutate the buffer as it
+ *  sees fit.  It typically does this by calling CFURLGetByteRangeForComponent to find the range
+ *  of interest in the buffer.  In that case bytesInserted is the amount to adjust that range,
+ *  and the function should modify that to account for any bytes it inserts or deletes.  If
+ *  the function modifies the buffer too much, it can return kCFNotFound to force the system
+ *  to re-create the URL from the buffer.
+ *  \param url The original URL to work on.
+ *  \param urlData The URL as a mutable buffer; the routine modifies this.
+ *  \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
+ *  \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
+ */
+
+typedef CFIndex (*CanonicalRequestStepFunction)(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted);
+
+/*! The post-scheme separate should be "://"; if that's not the case, fix it.
+ *  \param url The original URL to work on.
+ *  \param urlData The URL as a mutable buffer; the routine modifies this.
+ *  \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
+ *  \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
+ */
+
+static CFIndex FixPostSchemeSeparator(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
+{
+    CFRange     range;
+    uint8_t *   urlDataBytes;
+    NSUInteger  urlDataLength;
+    NSUInteger  cursor;
+    NSUInteger  separatorLength;
+    NSUInteger  expectedSeparatorLength;
+    
+    assert(url != nil);
+    assert(urlData != nil);
+    assert(bytesInserted >= 0);
+    
+    range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL);
+    if (range.location != kCFNotFound) {
+        assert(range.location >= 0);
+        assert(range.length >= 0);
+        
+        urlDataBytes  = [urlData mutableBytes];
+        urlDataLength = [urlData length];
+        
+        separatorLength = 0;
+        cursor = (NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length;
+        if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == ':') ) {
+            cursor += 1;
+            separatorLength += 1;
+            if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) {
+                cursor += 1;
+                separatorLength += 1;
+                if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) {
+                    cursor += 1;
+                    separatorLength += 1;
+                }
+            }
+        }
+#pragma unused(cursor)          // quietens an analyser warning
+        
+        expectedSeparatorLength = strlen("://");
+        if (separatorLength != expectedSeparatorLength) {
+            [urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length, separatorLength) withBytes:"://" length:expectedSeparatorLength];
+            bytesInserted = kCFNotFound;        // have to build everything now
+        }
+    }
+    
+    return bytesInserted;
+}
+
+/*! The scheme should be lower case; if it's not, make it so.
+ *  \param url The original URL to work on.
+ *  \param urlData The URL as a mutable buffer; the routine modifies this.
+ *  \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
+ *  \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
+ */
+
+static CFIndex LowercaseScheme(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
+{
+    CFRange     range;
+    uint8_t *   urlDataBytes;
+    CFIndex     i;
+    
+    assert(url != nil);
+    assert(urlData != nil);
+    assert(bytesInserted >= 0);
+    
+    range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL);
+    if (range.location != kCFNotFound) {
+        assert(range.location >= 0);
+        assert(range.length >= 0);
+        
+        urlDataBytes = [urlData mutableBytes];
+        for (i = range.location + bytesInserted; i < (range.location + bytesInserted + range.length); i++) {
+            urlDataBytes[i] = (uint8_t) tolower_l(urlDataBytes[i], NULL);
+        }
+    }
+    return bytesInserted;
+}
+
+/*! The host should be lower case; if it's not, make it so.
+ *  \param url The original URL to work on.
+ *  \param urlData The URL as a mutable buffer; the routine modifies this.
+ *  \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
+ *  \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
+ */
+
+static CFIndex LowercaseHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
+// The host should be lower case; if it's not, make it so.
+{
+    CFRange     range;
+    uint8_t *   urlDataBytes;
+    CFIndex     i;
+    
+    assert(url != nil);
+    assert(urlData != nil);
+    assert(bytesInserted >= 0);
+    
+    range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, NULL);
+    if (range.location != kCFNotFound) {
+        assert(range.location >= 0);
+        assert(range.length >= 0);
+        
+        urlDataBytes = [urlData mutableBytes];
+        for (i = range.location + bytesInserted; i < (range.location + bytesInserted + range.length); i++) {
+            urlDataBytes[i] = (uint8_t) tolower_l(urlDataBytes[i], NULL);
+        }
+    }
+    return bytesInserted;
+}
+
+/*! An empty host should be treated as "localhost" case; if it's not, make it so.
+ *  \param url The original URL to work on.
+ *  \param urlData The URL as a mutable buffer; the routine modifies this.
+ *  \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
+ *  \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
+ */
+
+static CFIndex FixEmptyHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
+{
+    CFRange     range;
+    CFRange     rangeWithSeparator;
+    
+    assert(url != nil);
+    assert(urlData != nil);
+    assert(bytesInserted >= 0);
+    
+    range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, &rangeWithSeparator);
+    if (range.length == 0) {
+        NSUInteger  localhostLength;
+        
+        /* Note:
+         *     We have updated this Apple provided (and JAHPAuthenticatingHTTPProtocol subsumed) code
+         *    to fix a bug in this function inherited from the original source. The comments below
+         *     detail the fix.
+         *
+         * Source: https://developer.apple.com/library/content/samplecode/CustomHTTPProtocol/Listings/CustomHTTPProtocol_Core_Code_CanonicalRequest_m.html
+         *
+         * Fix:
+         *    Removed `assert(range.location >= 0)` which fails every time because
+         *     if there is an empty host then range.location == kCFNotFound == -1.
+         * Fix:
+         *    Added rangeWithSeperator assertion. Length should always be non-negative.
+         *
+         * Please refer to the comments in CFURL.h for CFURLGetByteRangeForComponent:.
+         * An excerpt:
+         *     If non-NULL, rangeIncludingSeparators gives the range of component
+         *    including the sequences that separate component from the previous and
+         *    next components.  If there is no previous or next component, that end of
+         *    rangeIncludingSeparators will match the range of the component itself.
+         *    If url does not contain the given component type, (kCFNotFound, 0) is
+         *    returned, and rangeIncludingSeparators is set to the location where the
+         *    component would be inserted.
+         */
+        assert(range.length >= 0);
+        assert(rangeWithSeparator.length >= 0);
+        
+        localhostLength = strlen("localhost");
+        if (range.location != kCFNotFound) {
+            [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) range.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength];
+            bytesInserted += localhostLength;
+        } else if ( (rangeWithSeparator.location != kCFNotFound) && (rangeWithSeparator.length == 0) ) {
+            [urlData replaceBytesInRange:NSMakeRange((NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength];
+            bytesInserted += localhostLength;
+        }
+    }
+    return bytesInserted;
+}
+
+/*! Transform an empty URL path to "/".  For example, "http://www.apple.com" becomes "http://www.apple.com/".
+ *  \param url The original URL to work on.
+ *  \param urlData The URL as a mutable buffer; the routine modifies this.
+ *  \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
+ *  \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
+ */
+
+static CFIndex FixEmptyPath(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
+{
+    CFRange     range;
+    CFRange     rangeWithSeparator;
+    
+    assert(url != nil);
+    assert(urlData != nil);
+    assert(bytesInserted >= 0);
+    
+    range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPath, &rangeWithSeparator);
+    // The following is not a typo.  We use rangeWithSeparator to find where to insert the
+    // "/" and the range length to decide whether we /need/ to insert the "/".
+    if ( (rangeWithSeparator.location != kCFNotFound) && (range.length == 0) ) {
+        assert(range.location >= 0);
+        assert(range.length >= 0);
+        assert(rangeWithSeparator.location >= 0);
+        assert(rangeWithSeparator.length >= 0);
+        
+        [urlData replaceBytesInRange:NSMakeRange( (NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"/" length:1];
+        bytesInserted += 1;
+    }
+    return bytesInserted;
+}
+
+/*! If the user specified the default port (80 for HTTP, 443 for HTTPS), remove it from the URL.
+ *  \details Actually this code is disabled because the equivalent code in the default protocol
+ *  handler has also been disabled; some setups depend on get the port number in the URL, even if it
+ *  is the default.
+ *  \param url The original URL to work on.
+ *  \param urlData The URL as a mutable buffer; the routine modifies this.
+ *  \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
+ *  \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
+ */
+
+__attribute__((unused)) static CFIndex DeleteDefaultPort(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
+{
+    NSString *  scheme;
+    BOOL        isHTTP;
+    BOOL        isHTTPS;
+    CFRange     range;
+    uint8_t *   urlDataBytes;
+    NSString *  portNumberStr;
+    int         portNumber;
+    
+    assert(url != nil);
+    assert(urlData != nil);
+    assert(bytesInserted >= 0);
+    
+    scheme = [[url scheme] lowercaseString];
+    assert(scheme != nil);
+    
+    isHTTP  = [scheme isEqual:@"http" ];
+    isHTTPS = [scheme isEqual:@"https"];
+    
+    range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPort, NULL);
+    if (range.location != kCFNotFound) {
+        assert(range.location >= 0);
+        assert(range.length >= 0);
+        
+        urlDataBytes = [urlData mutableBytes];
+        
+        portNumberStr = [[NSString alloc] initWithBytes:&urlDataBytes[range.location + bytesInserted] length:(NSUInteger) range.length encoding:NSUTF8StringEncoding];
+        if (portNumberStr != nil) {
+            portNumber = [portNumberStr intValue];
+            if ( (isHTTP && (portNumber == 80)) || (isHTTPS && (portNumber == 443)) ) {
+                // -1 and +1 to account for the leading ":"
+                [urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted - 1, (NSUInteger) range.length + 1) withBytes:NULL length:0];
+                bytesInserted -= (range.length + 1);
+            }
+        }
+    }
+    return bytesInserted;
+}
+
+#pragma mark * Other request canonicalization
+
+/*! Canonicalise the request headers.
+ *  \param request The request to canonicalise.
+ */
+
+static void CanonicaliseHeaders(NSMutableURLRequest * request)
+{
+    // If there's no content type and the request is a POST with a body, add a default
+    // content type of "application/x-www-form-urlencoded".
+    
+    if ( ([request valueForHTTPHeaderField:@"Content-Type"] == nil)
+        && ([[request HTTPMethod] caseInsensitiveCompare:@"POST"] == NSOrderedSame)
+        && (([request HTTPBody] != nil) || ([request HTTPBodyStream] != nil)) ) {
+        [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
+    }
+    
+    // If there's no "Accept" header, add a default.
+    
+    if ([request valueForHTTPHeaderField:@"Accept"] == nil) {
+        [request setValue:@"*/*" forHTTPHeaderField:@"Accept"];
+    }
+    
+    // If there's not "Accept-Encoding" header, add a default.
+    
+    if ([request valueForHTTPHeaderField:@"Accept-Encoding"] == nil) {
+        [request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"];
+    }
+    
+    // If there's not an "Accept-Language" headre, add a default.  This is quite bogus; ideally we
+    // should derive the correct "Accept-Language" value from the langauge that the app is running
+    // in.  However, that's quite difficult to get right, so rather than show some general purpose
+    // code that might fail in some circumstances, I've decided to just hardwire US English.
+    // If you use this code in your own app you can customise it as you see fit.  One option might be
+    // to base this value on -[NSBundle preferredLocalizations], so that the web page comes back in
+    // the language that the app is running in.
+    
+    if ([request valueForHTTPHeaderField:@"Accept-Language"] == nil) {
+        [request setValue:@"en-us" forHTTPHeaderField:@"Accept-Language"];
+    }
+}
+
+#pragma mark * API
+
+extern NSMutableURLRequest * JAHPCanonicalRequestForRequest(NSURLRequest *request)
+{
+    NSMutableURLRequest *   result;
+    NSString *              scheme;
+    
+    assert(request != nil);
+    
+    // Make a mutable copy of the request.
+    
+    result = [request mutableCopy];
+    
+    // First up check that we're dealing with HTTP or HTTPS.  If not, do nothing (why were we
+    // we even called?).
+    
+    scheme = [[[request URL] scheme] lowercaseString];
+    assert(scheme != nil);
+    
+    if ( ! [scheme isEqual:@"http" ] && ! [scheme isEqual:@"https"]) {
+        assert(NO);
+    } else {
+        CFIndex         bytesInserted;
+        NSURL *         requestURL;
+        NSMutableData * urlData;
+        static const CanonicalRequestStepFunction kStepFunctions[] = {
+            FixPostSchemeSeparator,
+            LowercaseScheme,
+            LowercaseHost,
+            FixEmptyHost,
+            // DeleteDefaultPort,       -- The built-in canonicalizer has stopped doing this, so we don't do it either.
+            FixEmptyPath
+        };
+        size_t          stepIndex;
+        size_t          stepCount;
+        
+        // Canonicalise the URL by executing each of our step functions.
+        
+        bytesInserted = kCFNotFound;
+        urlData = nil;
+        requestURL = [request URL];
+        assert(requestURL != nil);
+        
+        stepCount = sizeof(kStepFunctions) / sizeof(*kStepFunctions);
+        for (stepIndex = 0; stepIndex < stepCount; stepIndex++) {
+            
+            // If we don't have valid URL data, create it from the URL.
+            
+            assert(requestURL != nil);
+            if (bytesInserted == kCFNotFound) {
+                NSData *    urlDataImmutable;
+                
+                urlDataImmutable = CFBridgingRelease( CFURLCreateData(NULL, (CFURLRef) requestURL, kCFStringEncodingUTF8, true) );
+                assert(urlDataImmutable != nil);
+                
+                urlData = [urlDataImmutable mutableCopy];
+                assert(urlData != nil);
+                
+                bytesInserted = 0;
+            }
+            assert(urlData != nil);
+            
+            // Run the step.
+            
+            bytesInserted = kStepFunctions[stepIndex](requestURL, urlData, bytesInserted);
+            
+            // Note: The following logging is useful when debugging this code.  Change the
+            // if expression to YES to enable it.
+            
+            if (/* DISABLES CODE */ (NO)) {
+                fprintf(stderr, "  [%zu] %.*s\n", stepIndex, (int) [urlData length], (const char *) [urlData bytes]);
+            }
+            
+            // If the step invalidated our URL (or we're on the last step, whereupon we'll need
+            // the URL outside of the loop), recreate the URL from the URL data.
+            
+            if ( (bytesInserted == kCFNotFound) || ((stepIndex + 1) == stepCount) ) {
+                requestURL = CFBridgingRelease( CFURLCreateWithBytes(NULL, [urlData bytes], (CFIndex) [urlData length], kCFStringEncodingUTF8, NULL) );
+                assert(requestURL != nil);
+                
+                urlData = nil;
+            }
+        }
+        
+        [result setURL:requestURL];
+        
+        // Canonicalise the headers.
+        
+        CanonicaliseHeaders(result);
+    }
+    
+    return result;
+}

+ 91 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPQNSURLSessionDemux.h

@@ -0,0 +1,91 @@
+/*
+ File: JAHPQNSURLSessionDemux.h
+ Abstract: A general class to demux NSURLSession delegate callbacks.
+ 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 <Foundation/Foundation.h>
+
+/*! A simple class for demultiplexing NSURLSession delegate callbacks to a per-task delegate object.
+ 
+ You initialise the class with a session configuration. After that you can create data tasks
+ within that session by calling -dataTaskWithRequest:delegate:modes:.  Any delegate callbacks
+ for that data task are redirected to the delegate on the thread that created the task in
+ one of the specified run loop modes.  That thread must run its run loop in order to get
+ these callbacks.
+ */
+
+@interface JAHPQNSURLSessionDemux : NSObject
+
+/*! Create a demultiplex for the specified session configuration.
+ *  \param configuration The session configuration to use; if nil, a default session is created.
+ *  \returns An initialised instance.
+ */
+
+- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration;
+
+@property (atomic, copy,   readonly ) NSURLSessionConfiguration *   configuration;  ///< A copy of the configuration passed to -initWithConfiguration:.
+@property (atomic, strong, readonly ) NSURLSession *                session;        ///< The session created from the configuration passed to -initWithConfiguration:.
+
+/*! Creates a new data task whose delegate callbacks are routed to the supplied delegate.
+ *  \details The callbacks are run on the current thread (that is, the thread that called this
+ *  method) in the specified modes.
+ *
+ *  The delegate is retained until the task completes, that is, until after your
+ *  -URLSession:task:didCompleteWithError: delegate callback returns.
+ *
+ *  The returned task is suspend.  You must resume the returned task for the task to
+ *  make progress.  Furthermore, it's not safe to simply discard the returned task
+ *  because in that case the task's delegate is never released.
+ *
+ *  \param request The request that the data task executes; must not be nil.
+ *  \param delegate The delegate to receive the data task's delegate callbacks; must not be nil.
+ *  \param modes The run loop modes in which to run the data task's delegate callbacks; if nil or
+ *  empty, the default run loop mode (NSDefaultRunLoopMode is used).
+ *  \returns A suspended data task that you must resume.
+ */
+
+- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes;
+
+@end

+ 332 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPQNSURLSessionDemux.m

@@ -0,0 +1,332 @@
+/*
+ File: JAHPQNSURLSessionDemux.m
+ Abstract: A general class to demux NSURLSession delegate callbacks.
+ 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 "JAHPQNSURLSessionDemux.h"
+
+@interface JAHPQNSURLSessionDemuxTaskInfo : NSObject
+
+- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes;
+
+@property (atomic, strong, readonly ) NSURLSessionDataTask *        task;
+@property (atomic, strong, readonly ) id<NSURLSessionDataDelegate>  delegate;
+@property (atomic, strong, readonly ) NSThread *                    thread;
+@property (atomic, copy,   readonly ) NSArray *                     modes;
+
+- (void)performBlock:(dispatch_block_t)block;
+
+- (void)invalidate;
+
+@end
+
+@interface JAHPQNSURLSessionDemuxTaskInfo ()
+
+@property (atomic, strong, readwrite) id<NSURLSessionDataDelegate>  delegate;
+@property (atomic, strong, readwrite) NSThread *                    thread;
+
+@end
+
+@implementation JAHPQNSURLSessionDemuxTaskInfo
+
+- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
+{
+    assert(task != nil);
+    assert(delegate != nil);
+    assert(modes != nil);
+    
+    self = [super init];
+    if (self != nil) {
+        self->_task = task;
+        self->_delegate = delegate;
+        self->_thread = [NSThread currentThread];
+        self->_modes = [modes copy];
+    }
+    return self;
+}
+
+- (void)performBlock:(dispatch_block_t)block
+{
+    assert(self.delegate != nil);
+    assert(self.thread != nil);
+    [self performSelector:@selector(performBlockOnClientThread:) onThread:self.thread withObject:[block copy] waitUntilDone:NO modes:self.modes];
+}
+
+- (void)performBlockOnClientThread:(dispatch_block_t)block
+{
+    assert([NSThread currentThread] == self.thread);
+    block();
+}
+
+- (void)invalidate
+{
+    self.delegate = nil;
+    self.thread = nil;
+}
+
+@end
+
+@interface JAHPQNSURLSessionDemux () <NSURLSessionDataDelegate>
+
+@property (atomic, strong, readonly ) NSMutableDictionary * taskInfoByTaskID;       // keys NSURLSessionTask taskIdentifier, values are SessionManager
+@property (atomic, strong, readonly ) NSOperationQueue *    sessionDelegateQueue;
+
+@end
+
+@implementation JAHPQNSURLSessionDemux
+
+- (instancetype)init
+{
+    return [self initWithConfiguration:nil];
+}
+
+- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration
+{
+    // configuration may be nil
+    self = [super init];
+    if (self != nil) {
+        if (configuration == nil) {
+            configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
+        }
+        self->_configuration = [configuration copy];
+        
+        self->_taskInfoByTaskID = [[NSMutableDictionary alloc] init];
+        
+        self->_sessionDelegateQueue = [[NSOperationQueue alloc] init];
+        [self->_sessionDelegateQueue setMaxConcurrentOperationCount:1];
+        [self->_sessionDelegateQueue setName:@"JAHPQNSURLSessionDemux"];
+        
+        self->_session = [NSURLSession sessionWithConfiguration:self->_configuration delegate:self delegateQueue:self->_sessionDelegateQueue];
+        self->_session.sessionDescription = @"JAHPQNSURLSessionDemux";
+    }
+    return self;
+}
+
+- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
+{
+    NSURLSessionDataTask *          task;
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    assert(request != nil);
+    assert(delegate != nil);
+    // modes may be nil
+    
+    if ([modes count] == 0) {
+        modes = @[ NSDefaultRunLoopMode ];
+    }
+    
+    task = [self.session dataTaskWithRequest:request];
+    assert(task != nil);
+    
+    taskInfo = [[JAHPQNSURLSessionDemuxTaskInfo alloc] initWithTask:task delegate:delegate modes:modes];
+    
+    @synchronized (self) {
+        self.taskInfoByTaskID[@(task.taskIdentifier)] = taskInfo;
+    }
+    
+    return task;
+}
+
+- (JAHPQNSURLSessionDemuxTaskInfo *)taskInfoForTask:(NSURLSessionTask *)task
+{
+    // rdar://21484589
+    // This is called from each NSURLSessionTaskDelegate
+    // method implemented by JAHPQNSURLSessionDemux,
+    // which are called from the NSURLSession delegateQueue,
+    // which is a different thread than self.clientThread.
+    // It is possible that -stopLoading was called on self.clientThread
+    // just before this method if so, we cannot make the
+    // assertion (task != nil).
+
+    // rdar://24042545
+    // URLSession:task:didCompleteWithError: should always
+    // be the last NSURLSessionTaskDelegate method called.
+    // Although, it is possible that this is not always
+    // the case. Therefore we cannot make the assertion
+    // (self.taskInfoByTaskID[@(task.taskIdentifier)] != nil)
+    // because [self.taskInfoByTaskID removeObjectForKey:@(taskInfo.task.taskIdentifier)]
+    // is called in URLSession:task:didCompleteWithError:.
+    JAHPQNSURLSessionDemuxTaskInfo *    result;
+
+    @synchronized (self) {
+        result = self.taskInfoByTaskID[@(task.taskIdentifier)];
+    }
+    return result;
+}
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:task];
+    if (taskInfo && [taskInfo.delegate respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session task:task willPerformHTTPRedirection:response newRequest:newRequest completionHandler:completionHandler];
+        }];
+    } else {
+        completionHandler(newRequest);
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:task];
+    if (taskInfo && [taskInfo.delegate respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
+        }];
+    } else {
+        completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:task];
+    if (taskInfo && [taskInfo.delegate respondsToSelector:@selector(URLSession:task:needNewBodyStream:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session task:task needNewBodyStream:completionHandler];
+        }];
+    } else {
+        completionHandler(nil);
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:task];
+    if (taskInfo && [taskInfo.delegate respondsToSelector:@selector(URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session task:task didSendBodyData:bytesSent totalBytesSent:totalBytesSent totalBytesExpectedToSend:totalBytesExpectedToSend];
+        }];
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:task];
+    
+    // This is our last delegate callback so we remove our task info record.
+    
+    @synchronized (self) {
+        [self.taskInfoByTaskID removeObjectForKey:@(taskInfo.task.taskIdentifier)];
+    }
+    
+    // Call the delegate if required.  In that case we invalidate the task info on the client thread
+    // after calling the delegate, otherwise the client thread side of the -performBlock: code can
+    // find itself with an invalidated task info.
+    
+    if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session task:task didCompleteWithError:error];
+            [taskInfo invalidate];
+        }];
+    } else {
+        [taskInfo invalidate];
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:dataTask];
+    if (taskInfo && [taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
+        }];
+    } else {
+        completionHandler(NSURLSessionResponseAllow);
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:dataTask];
+    if (taskInfo && [taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didBecomeDownloadTask:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session dataTask:dataTask didBecomeDownloadTask:downloadTask];
+        }];
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:dataTask];
+    if (taskInfo && [taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session dataTask:dataTask didReceiveData:data];
+        }];
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
+{
+    JAHPQNSURLSessionDemuxTaskInfo *    taskInfo;
+    
+    taskInfo = [self taskInfoForTask:dataTask];
+    if (taskInfo && [taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
+        [taskInfo performBlock:^{
+            [taskInfo.delegate URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
+        }];
+    } else {
+        completionHandler(proposedResponse);
+    }
+}
+
+@end
+

+ 90 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/README.md

@@ -0,0 +1,90 @@
+# iOS Library Sample App: TunneledWebView
+
+## Tunneling UIWebView
+
+*Note: this approach does not work with WKWebView (see [http://www.openradar.me/17190141](http://www.openradar.me/17190141)).*
+
+This app tunnels UIWebView traffic by proxying requests through the local Psiphon proxies created by [PsiphonTunnel](https://github.com/Psiphon-Labs/psiphon-tunnel-core/tree/master/MobileLibrary/iOS/PsiphonTunnel).
+The listening Psiphon proxy ports can be obtained via TunneledAppDelegate delegate callbacks (see `onListeningSocksProxyPort` and `onListeningHttpProxyPort` in `AppDelegate.swift`).
+
+This is accomplished by registering `NSURLProtocol` subclass `JAHPAuthenticatingHTTPProtocol` with `NSURLProtocol`.
+`JAHPAuthenticatingHTTPProtocol` is then configured to use the local Psiphon proxies.
+This is done by setting the [connectionProxyDictionary](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1411499-connectionproxydictionary?language=objc) of [NSURLSessionConfiguration](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration).
+See [`+ (JAHPQNSURLSessionDemux *)sharedDemux`](https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/c9c4834fba5e7a8b675c3ae493ac17b5975ab0fb/MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m#L157) in `JAHPAuthenticatingHTTPProtocol.m`.
+
+We use a slightly modified version of JiveAuthenticatingProtocol (https://github.com/jivesoftware/JiveAuthenticatingHTTPProtocol), which in turn is largely based on [Apple's CustomHTTPProtocol example](https://developer.apple.com/library/content/samplecode/CustomHTTPProtocol/Introduction/Intro.html). 
+
+## *\*\* Caveats \*\*\*
+
+### Challenges
+
+***NSURLProtocol is only partially supported by UIWebView (https://bugs.webkit.org/show_bug.cgi?id=138169) and in 
+some versions of iOS audio and video are fetched out of process in mediaserverd and therefore are
+not intercepted by NSURLProtocol.***
+
+*In our limited testing iOS 9/10 leak and iOS 11 does not leak.*
+
+### Workarounds
+
+***It is worth noting that this fix is inexact and may not always work. If one has control over the HTML being rendered and resources being fetched with XHR it is preferable to alter 
+the media source URLs directly beforehand instead of relying on the javascript injection trick.***
+
+***This is a description of a workaround used in the [Psiphon Browser iOS app](https://github.com/Psiphon-Inc/endless) and not of what is implemented in TunneledWebView.
+TunneledWebView *does NOT* attempt to tunnel all audio/video content in UIWebView. This is only a hack which allows tunneling
+audio and video in UIWebView on versions of iOS which fetch audio/video out of process.***
+
+#### Background
+In [PsiphonBrowser](https://github.com/Psiphon-Inc/endless) we have implemented a workaround for audio and video being 
+fetched out of process.
+
+[PsiphonTunnel's](https://github.com/Psiphon-Labs/psiphon-tunnel-core/tree/master/MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel)
+HTTP Proxy also offers a ["URL proxy (reverse proxy)"](https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/631099d086c7c554a590b0cb76766be6dce94ef9/psiphon/httpProxy.go#L45-L70) 
+mode that relays requests for HTTP or HTTPS or URLs specified in the proxy request path. 
+ 
+This reverse proxy can be used by constructing a URL such as `http://127.0.0.1:<proxy-port>/tunneled-rewrite/<origin media URL>?m3u8=true`.
+
+When the retrieved resource is detected to be a [M3U8](https://en.wikipedia.org/wiki/M3U#M3U8) playlist a rewriting rule is applied to ensure all the URL entries
+are rewritten to use the same reverse proxy. Otherwise it will be returned unmodified.
+
+#### Fix
+
+* Media element URLs are rewritten to use the URL proxy (reverse proxy).
+* This is done by [injecting javascript](https://github.com/Psiphon-Inc/endless/blob/b0c33b4bbd917467a849ad8c51a225c2d4dab260/Endless/Resources/injected.js#L379-L408) 
+into the HTML [as it is being loaded](https://github.com/Psiphon-Inc/endless/blob/b0c33b4bbd917467a849ad8c51a225c2d4dab260/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m#L1274-L1280) 
+which [rewrites media URLs to use the URL proxy (reverse proxy)](https://github.com/Psiphon-Inc/endless/blob/b0c33b4bbd917467a849ad8c51a225c2d4dab260/Endless/Resources/injected.js#L319-L377).
+* If a [CSP](https://en.wikipedia.org/wiki/Content_Security_Policy) 
+is found in the header of the response, we need to modify it to allow our injected javascript to run.
+  * This is done by [modifying the
+CSP](https://github.com/Psiphon-Inc/endless/blob/b0c33b4bbd917467a849ad8c51a225c2d4dab260/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m#L1184-L1228) 
+to include a nonce generated for our injected javascript, which is [included in the script tag](https://github.com/Psiphon-Inc/endless/blob/b0c33b4bbd917467a849ad8c51a225c2d4dab260/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m#L1276).
+
+## Configuring, Building, Running
+
+The sample app requires some extra files and configuration before building.
+
+### Get the framework.
+
+1. Get the latest iOS release from the project's [Releases](https://github.com/Psiphon-Labs/psiphon-tunnel-core/releases) page.
+2. Extract the archive. 
+2. Copy `PsiphonTunnel.framework` into the `TunneledWebView` directory.
+
+### Get the configuration.
+
+1. Contact Psiphon Inc. to obtain configuration values to use in your app. 
+   (This is requried to use the Psiphon network.)
+2. Make a copy of `TunneledWebView/psiphon-config.json.stub`, 
+   removing the `.stub` extension.
+3. Edit `psiphon-config.json`. Remove the comments and fill in the values with 
+   those received from Psiphon Inc. The `"ClientVersion"` value is up to you.
+
+### Ready!
+
+TunneledWebView should now compile and run.
+
+### Loading different URLs
+
+Just update `urlString = "https://freegeoip.net"` in `onConnected` to load a different URL into `UIWebView` with TunneledWebView.
+
+## License
+
+See the [LICENSE](../LICENSE) file.

+ 655 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/project.pbxproj

@@ -0,0 +1,655 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		4E0CA9681FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E0CA9611FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.m */; };
+		4E0CA9691FDE554B00B48BCA /* JAHPCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E0CA9631FDE554B00B48BCA /* JAHPCacheStoragePolicy.m */; };
+		4E0CA96A1FDE554B00B48BCA /* JAHPCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E0CA9651FDE554B00B48BCA /* JAHPCanonicalRequest.m */; };
+		4E0CA96B1FDE554B00B48BCA /* JAHPQNSURLSessionDemux.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E0CA9671FDE554B00B48BCA /* JAHPQNSURLSessionDemux.m */; };
+		4EE9CDD91FE0830600BCE310 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 4EE9CDD81FE0830600BCE310 /* README.md */; };
+		662658EE1DCB8CF300872F6C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662658ED1DCB8CF300872F6C /* AppDelegate.swift */; };
+		662658F01DCB8CF300872F6C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662658EF1DCB8CF300872F6C /* ViewController.swift */; };
+		662658F31DCB8CF300872F6C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 662658F11DCB8CF300872F6C /* Main.storyboard */; };
+		662658F51DCB8CF300872F6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 662658F41DCB8CF300872F6C /* Assets.xcassets */; };
+		662658F81DCB8CF300872F6C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 662658F61DCB8CF300872F6C /* LaunchScreen.storyboard */; };
+		662659031DCB8CF400872F6C /* TunneledWebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662659021DCB8CF400872F6C /* TunneledWebViewTests.swift */; };
+		6626590E1DCB8CF400872F6C /* TunneledWebViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6626590D1DCB8CF400872F6C /* TunneledWebViewUITests.swift */; };
+		662659211DCBC7C300872F6C /* PsiphonTunnel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 662659201DCBC7C300872F6C /* PsiphonTunnel.framework */; };
+		662659231DCBC8D800872F6C /* PsiphonTunnel.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 662659201DCBC7C300872F6C /* PsiphonTunnel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		6682D90E1EB1334000329958 /* psiphon-embedded-server-entries.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6682D90D1EB1334000329958 /* psiphon-embedded-server-entries.txt */; };
+		6688DBB61DCD684B00721A9E /* psiphon-config.json in Resources */ = {isa = PBXBuildFile; fileRef = 6688DBB51DCD684B00721A9E /* psiphon-config.json */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		662658FF1DCB8CF400872F6C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 662658E21DCB8CF300872F6C /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 662658E91DCB8CF300872F6C;
+			remoteInfo = TunneledWebView;
+		};
+		6626590A1DCB8CF400872F6C /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 662658E21DCB8CF300872F6C /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 662658E91DCB8CF300872F6C;
+			remoteInfo = TunneledWebView;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		662659221DCBC8CB00872F6C /* CopyFiles */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+				662659231DCBC8D800872F6C /* PsiphonTunnel.framework in CopyFiles */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		4E0CA9601FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JAHPAuthenticatingHTTPProtocol.h; sourceTree = "<group>"; };
+		4E0CA9611FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JAHPAuthenticatingHTTPProtocol.m; sourceTree = "<group>"; };
+		4E0CA9621FDE554B00B48BCA /* JAHPCacheStoragePolicy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JAHPCacheStoragePolicy.h; sourceTree = "<group>"; };
+		4E0CA9631FDE554B00B48BCA /* JAHPCacheStoragePolicy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JAHPCacheStoragePolicy.m; sourceTree = "<group>"; };
+		4E0CA9641FDE554B00B48BCA /* JAHPCanonicalRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JAHPCanonicalRequest.h; sourceTree = "<group>"; };
+		4E0CA9651FDE554B00B48BCA /* JAHPCanonicalRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JAHPCanonicalRequest.m; sourceTree = "<group>"; };
+		4E0CA9661FDE554B00B48BCA /* JAHPQNSURLSessionDemux.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JAHPQNSURLSessionDemux.h; sourceTree = "<group>"; };
+		4E0CA9671FDE554B00B48BCA /* JAHPQNSURLSessionDemux.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JAHPQNSURLSessionDemux.m; sourceTree = "<group>"; };
+		4E5A8DF51FDA7541009F8702 /* TunneledWebView-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TunneledWebView-Bridging-Header.h"; sourceTree = "<group>"; };
+		4EE9CDD81FE0830600BCE310 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; };
+		662658EA1DCB8CF300872F6C /* TunneledWebView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TunneledWebView.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		662658ED1DCB8CF300872F6C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		662658EF1DCB8CF300872F6C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
+		662658F21DCB8CF300872F6C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		662658F41DCB8CF300872F6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		662658F71DCB8CF300872F6C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		662658F91DCB8CF300872F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		662658FE1DCB8CF400872F6C /* TunneledWebViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TunneledWebViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		662659021DCB8CF400872F6C /* TunneledWebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunneledWebViewTests.swift; sourceTree = "<group>"; };
+		662659041DCB8CF400872F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		662659091DCB8CF400872F6C /* TunneledWebViewUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TunneledWebViewUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		6626590D1DCB8CF400872F6C /* TunneledWebViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunneledWebViewUITests.swift; sourceTree = "<group>"; };
+		6626590F1DCB8CF400872F6C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		662659201DCBC7C300872F6C /* PsiphonTunnel.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = PsiphonTunnel.framework; sourceTree = "<group>"; };
+		6682D90D1EB1334000329958 /* psiphon-embedded-server-entries.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "psiphon-embedded-server-entries.txt"; sourceTree = "<group>"; };
+		6688DBB51DCD684B00721A9E /* psiphon-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "psiphon-config.json"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		662658E71DCB8CF300872F6C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				662659211DCBC7C300872F6C /* PsiphonTunnel.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662658FB1DCB8CF400872F6C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662659061DCB8CF400872F6C /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		4E0CA95F1FDE554B00B48BCA /* JiveAuthenticatingHTTPProtocol */ = {
+			isa = PBXGroup;
+			children = (
+				4E0CA9601FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.h */,
+				4E0CA9611FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.m */,
+				4E0CA9621FDE554B00B48BCA /* JAHPCacheStoragePolicy.h */,
+				4E0CA9631FDE554B00B48BCA /* JAHPCacheStoragePolicy.m */,
+				4E0CA9641FDE554B00B48BCA /* JAHPCanonicalRequest.h */,
+				4E0CA9651FDE554B00B48BCA /* JAHPCanonicalRequest.m */,
+				4E0CA9661FDE554B00B48BCA /* JAHPQNSURLSessionDemux.h */,
+				4E0CA9671FDE554B00B48BCA /* JAHPQNSURLSessionDemux.m */,
+			);
+			name = JiveAuthenticatingHTTPProtocol;
+			path = External/JiveAuthenticatingHTTPProtocol;
+			sourceTree = SOURCE_ROOT;
+		};
+		662658E11DCB8CF300872F6C = {
+			isa = PBXGroup;
+			children = (
+				662658EC1DCB8CF300872F6C /* TunneledWebView */,
+				662659011DCB8CF400872F6C /* TunneledWebViewTests */,
+				6626590C1DCB8CF400872F6C /* TunneledWebViewUITests */,
+				662658EB1DCB8CF300872F6C /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		662658EB1DCB8CF300872F6C /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				662658EA1DCB8CF300872F6C /* TunneledWebView.app */,
+				662658FE1DCB8CF400872F6C /* TunneledWebViewTests.xctest */,
+				662659091DCB8CF400872F6C /* TunneledWebViewUITests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		662658EC1DCB8CF300872F6C /* TunneledWebView */ = {
+			isa = PBXGroup;
+			children = (
+				4EE9CDD81FE0830600BCE310 /* README.md */,
+				4E5A8DF51FDA7541009F8702 /* TunneledWebView-Bridging-Header.h */,
+				662658ED1DCB8CF300872F6C /* AppDelegate.swift */,
+				662658EF1DCB8CF300872F6C /* ViewController.swift */,
+				662658F11DCB8CF300872F6C /* Main.storyboard */,
+				662658F41DCB8CF300872F6C /* Assets.xcassets */,
+				662658F61DCB8CF300872F6C /* LaunchScreen.storyboard */,
+				662658F91DCB8CF300872F6C /* Info.plist */,
+				6688DBB51DCD684B00721A9E /* psiphon-config.json */,
+				6682D90D1EB1334000329958 /* psiphon-embedded-server-entries.txt */,
+				4E0CA95F1FDE554B00B48BCA /* JiveAuthenticatingHTTPProtocol */,
+				662659201DCBC7C300872F6C /* PsiphonTunnel.framework */,
+			);
+			path = TunneledWebView;
+			sourceTree = "<group>";
+		};
+		662659011DCB8CF400872F6C /* TunneledWebViewTests */ = {
+			isa = PBXGroup;
+			children = (
+				662659021DCB8CF400872F6C /* TunneledWebViewTests.swift */,
+				662659041DCB8CF400872F6C /* Info.plist */,
+			);
+			path = TunneledWebViewTests;
+			sourceTree = "<group>";
+		};
+		6626590C1DCB8CF400872F6C /* TunneledWebViewUITests */ = {
+			isa = PBXGroup;
+			children = (
+				6626590D1DCB8CF400872F6C /* TunneledWebViewUITests.swift */,
+				6626590F1DCB8CF400872F6C /* Info.plist */,
+			);
+			path = TunneledWebViewUITests;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		662658E91DCB8CF300872F6C /* TunneledWebView */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 662659121DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebView" */;
+			buildPhases = (
+				662658E61DCB8CF300872F6C /* Sources */,
+				662658E71DCB8CF300872F6C /* Frameworks */,
+				662658E81DCB8CF300872F6C /* Resources */,
+				662659221DCBC8CB00872F6C /* CopyFiles */,
+				6685BDD71E300A7800F0E414 /* ShellScript */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = TunneledWebView;
+			productName = TunneledWebView;
+			productReference = 662658EA1DCB8CF300872F6C /* TunneledWebView.app */;
+			productType = "com.apple.product-type.application";
+		};
+		662658FD1DCB8CF400872F6C /* TunneledWebViewTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 662659151DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebViewTests" */;
+			buildPhases = (
+				662658FA1DCB8CF400872F6C /* Sources */,
+				662658FB1DCB8CF400872F6C /* Frameworks */,
+				662658FC1DCB8CF400872F6C /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				662659001DCB8CF400872F6C /* PBXTargetDependency */,
+			);
+			name = TunneledWebViewTests;
+			productName = TunneledWebViewTests;
+			productReference = 662658FE1DCB8CF400872F6C /* TunneledWebViewTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+		662659081DCB8CF400872F6C /* TunneledWebViewUITests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 662659181DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebViewUITests" */;
+			buildPhases = (
+				662659051DCB8CF400872F6C /* Sources */,
+				662659061DCB8CF400872F6C /* Frameworks */,
+				662659071DCB8CF400872F6C /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				6626590B1DCB8CF400872F6C /* PBXTargetDependency */,
+			);
+			name = TunneledWebViewUITests;
+			productName = TunneledWebViewUITests;
+			productReference = 662659091DCB8CF400872F6C /* TunneledWebViewUITests.xctest */;
+			productType = "com.apple.product-type.bundle.ui-testing";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		662658E21DCB8CF300872F6C /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastSwiftUpdateCheck = 0800;
+				LastUpgradeCheck = 0900;
+				ORGANIZATIONNAME = "Psiphon Inc.";
+				TargetAttributes = {
+					662658E91DCB8CF300872F6C = {
+						CreatedOnToolsVersion = 8.0;
+						DevelopmentTeam = Q6HLNEX92A;
+						ProvisioningStyle = Automatic;
+					};
+					662658FD1DCB8CF400872F6C = {
+						CreatedOnToolsVersion = 8.0;
+						DevelopmentTeam = Q6HLNEX92A;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 662658E91DCB8CF300872F6C;
+					};
+					662659081DCB8CF400872F6C = {
+						CreatedOnToolsVersion = 8.0;
+						DevelopmentTeam = Q6HLNEX92A;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 662658E91DCB8CF300872F6C;
+					};
+				};
+			};
+			buildConfigurationList = 662658E51DCB8CF300872F6C /* Build configuration list for PBXProject "TunneledWebView" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 662658E11DCB8CF300872F6C;
+			productRefGroup = 662658EB1DCB8CF300872F6C /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				662658E91DCB8CF300872F6C /* TunneledWebView */,
+				662658FD1DCB8CF400872F6C /* TunneledWebViewTests */,
+				662659081DCB8CF400872F6C /* TunneledWebViewUITests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		662658E81DCB8CF300872F6C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				4EE9CDD91FE0830600BCE310 /* README.md in Resources */,
+				662658F81DCB8CF300872F6C /* LaunchScreen.storyboard in Resources */,
+				662658F51DCB8CF300872F6C /* Assets.xcassets in Resources */,
+				6682D90E1EB1334000329958 /* psiphon-embedded-server-entries.txt in Resources */,
+				662658F31DCB8CF300872F6C /* Main.storyboard in Resources */,
+				6688DBB61DCD684B00721A9E /* psiphon-config.json in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662658FC1DCB8CF400872F6C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662659071DCB8CF400872F6C /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		6685BDD71E300A7800F0E414 /* ShellScript */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "bash \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/PsiphonTunnel.framework/strip-frameworks.sh\"";
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		662658E61DCB8CF300872F6C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				662658F01DCB8CF300872F6C /* ViewController.swift in Sources */,
+				4E0CA96A1FDE554B00B48BCA /* JAHPCanonicalRequest.m in Sources */,
+				4E0CA96B1FDE554B00B48BCA /* JAHPQNSURLSessionDemux.m in Sources */,
+				662658EE1DCB8CF300872F6C /* AppDelegate.swift in Sources */,
+				4E0CA9681FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.m in Sources */,
+				4E0CA9691FDE554B00B48BCA /* JAHPCacheStoragePolicy.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662658FA1DCB8CF400872F6C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				662659031DCB8CF400872F6C /* TunneledWebViewTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		662659051DCB8CF400872F6C /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				6626590E1DCB8CF400872F6C /* TunneledWebViewUITests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		662659001DCB8CF400872F6C /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 662658E91DCB8CF300872F6C /* TunneledWebView */;
+			targetProxy = 662658FF1DCB8CF400872F6C /* PBXContainerItemProxy */;
+		};
+		6626590B1DCB8CF400872F6C /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 662658E91DCB8CF300872F6C /* TunneledWebView */;
+			targetProxy = 6626590A1DCB8CF400872F6C /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		662658F11DCB8CF300872F6C /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				662658F21DCB8CF300872F6C /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		662658F61DCB8CF300872F6C /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				662658F71DCB8CF300872F6C /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		662659101DCB8CF400872F6C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_SUSPICIOUS_MOVES = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_BITCODE = YES;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		662659111DCB8CF400872F6C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_SUSPICIOUS_MOVES = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_BITCODE = YES;
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		662659131DCB8CF400872F6C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/TunneledWebView",
+				);
+				INFOPLIST_FILE = TunneledWebView/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebView;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_BITCODE_FROM_COPIED_FILES = NO;
+				SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/TunneledWebView/TunneledWebView-Bridging-Header.h";
+				SWIFT_VERSION = 3.0;
+			};
+			name = Debug;
+		};
+		662659141DCB8CF400872F6C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				DEFINES_MODULE = YES;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				ENABLE_BITCODE = NO;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/TunneledWebView",
+				);
+				INFOPLIST_FILE = TunneledWebView/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebView;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_BITCODE_FROM_COPIED_FILES = NO;
+				SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/TunneledWebView/TunneledWebView-Bridging-Header.h";
+				SWIFT_VERSION = 3.0;
+			};
+			name = Release;
+		};
+		662659161DCB8CF400872F6C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = TunneledWebViewTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebViewTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 3.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TunneledWebView.app/TunneledWebView";
+			};
+			name = Debug;
+		};
+		662659171DCB8CF400872F6C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = TunneledWebViewTests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebViewTests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 3.0;
+				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TunneledWebView.app/TunneledWebView";
+			};
+			name = Release;
+		};
+		662659191DCB8CF400872F6C /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = TunneledWebViewUITests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebViewUITests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 3.0;
+				TEST_TARGET_NAME = TunneledWebView;
+			};
+			name = Debug;
+		};
+		6626591A1DCB8CF400872F6C /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				INFOPLIST_FILE = TunneledWebViewUITests/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebViewUITests;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_VERSION = 3.0;
+				TEST_TARGET_NAME = TunneledWebView;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		662658E51DCB8CF300872F6C /* Build configuration list for PBXProject "TunneledWebView" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				662659101DCB8CF400872F6C /* Debug */,
+				662659111DCB8CF400872F6C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		662659121DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebView" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				662659131DCB8CF400872F6C /* Debug */,
+				662659141DCB8CF400872F6C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		662659151DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebViewTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				662659161DCB8CF400872F6C /* Debug */,
+				662659171DCB8CF400872F6C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		662659181DCB8CF400872F6C /* Build configuration list for PBXNativeTarget "TunneledWebViewUITests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				662659191DCB8CF400872F6C /* Debug */,
+				6626591A1DCB8CF400872F6C /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 662658E21DCB8CF300872F6C /* Project object */;
+}

+ 7 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:/Users/miro/myforks/psiphon-tunnel-core/MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj">
+   </FileRef>
+</Workspace>

+ 113 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/xcshareddata/xcschemes/TunneledWebView.xcscheme

@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0900"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "662658E91DCB8CF300872F6C"
+               BuildableName = "TunneledWebView.app"
+               BlueprintName = "TunneledWebView"
+               ReferencedContainer = "container:TunneledWebView.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      language = ""
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "662658FD1DCB8CF400872F6C"
+               BuildableName = "TunneledWebViewTests.xctest"
+               BlueprintName = "TunneledWebViewTests"
+               ReferencedContainer = "container:TunneledWebView.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "662659081DCB8CF400872F6C"
+               BuildableName = "TunneledWebViewUITests.xctest"
+               BlueprintName = "TunneledWebViewUITests"
+               ReferencedContainer = "container:TunneledWebView.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "662658E91DCB8CF300872F6C"
+            BuildableName = "TunneledWebView.app"
+            BlueprintName = "TunneledWebView"
+            ReferencedContainer = "container:TunneledWebView.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      language = ""
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "662658E91DCB8CF300872F6C"
+            BuildableName = "TunneledWebView.app"
+            BlueprintName = "TunneledWebView"
+            ReferencedContainer = "container:TunneledWebView.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "662658E91DCB8CF300872F6C"
+            BuildableName = "TunneledWebView.app"
+            BlueprintName = "TunneledWebView"
+            ReferencedContainer = "container:TunneledWebView.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 181 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/AppDelegate.swift

@@ -0,0 +1,181 @@
+//
+//  AppDelegate.swift
+//  TunneledWebView
+//
+/*
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+import UIKit
+
+import PsiphonTunnel
+
+
+@UIApplicationMain
+@objc class AppDelegate: UIResponder, UIApplicationDelegate, JAHPAuthenticatingHTTPProtocolDelegate {
+
+    var window: UIWindow?
+    var socksProxyPort: Int = 0
+    var httpProxyPort: Int = 0
+
+    // The instance of PsiphonTunnel we'll use for connecting.
+    var psiphonTunnel: PsiphonTunnel?
+
+    class func sharedDelegate() -> AppDelegate {
+        var delegate: AppDelegate?
+        if (Thread.isMainThread) {
+            delegate = UIApplication.shared.delegate as? AppDelegate
+        } else {
+            DispatchQueue.main.sync {
+                delegate = UIApplication.shared.delegate as? AppDelegate
+            }
+        }
+        return delegate!
+    }
+
+    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+        // Override point for customization after application launch.
+
+        // Set the class delegate and register NSURL subclass
+        // JAHPAuthenticatingHTTPProtocol with NSURLProtocol.
+        // See comments for `setDelegate` and `start` in
+        // JAHPAuthenticatingHTTPProtocol.h
+        /*******************************************************/
+        /*****                                             *****/
+        /*****               !!! WARNING !!!               *****/
+        /*****                                             *****/
+        /*******************************************************/
+        /*****                                             *****/
+        /*****  This methood of proxying UIWebView is not  *****/
+        /*****  officially supported and requires extra    *****/
+        /*****  steps to proxy audio / video content.      *****/
+        /*****  Otherwise audio / video fetching may be    *****/
+        /*****  untunneled!                                *****/
+        /*****                                             *****/
+        /*****  It is strongly advised that you read the   *****/
+        /*****  "Caveats" section of README.md before      *****/
+        /*****  using PsiphonTunnel to proxy UIWebView     *****/
+        /*****  traffic.                                   *****/
+        /*****                                             *****/
+        /*******************************************************/
+        JAHPAuthenticatingHTTPProtocol.setDelegate(self)
+        JAHPAuthenticatingHTTPProtocol.start()
+
+        self.psiphonTunnel = PsiphonTunnel.newPsiphonTunnel(self)
+
+        return true
+    }
+
+    func applicationWillResignActive(_ application: UIApplication) {
+        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
+    }
+
+    func applicationDidEnterBackground(_ application: UIApplication) {
+        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
+        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+    }
+
+    func applicationWillEnterForeground(_ application: UIApplication) {
+        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
+    }
+
+    func applicationDidBecomeActive(_ application: UIApplication) {
+        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+
+        DispatchQueue.global(qos: .default).async {
+            // Start up the tunnel and begin connecting.
+            // This could be started elsewhere or earlier.
+            NSLog("Starting tunnel")
+
+            guard let success = self.psiphonTunnel?.start(true), success else {
+                NSLog("psiphonTunnel.start returned false")
+                return
+            }
+
+            // The Psiphon Library exposes reachability functions, which can be used for detecting internet status.
+            let reachability = Reachability.forInternetConnection()
+            let networkStatus = reachability?.currentReachabilityStatus()
+            NSLog("Internet is reachable? \(networkStatus != NotReachable)")
+        }
+    }
+
+    func applicationWillTerminate(_ application: UIApplication) {
+        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+
+        // Clean up the tunnel
+        NSLog("Stopping tunnel")
+        self.psiphonTunnel?.stop()
+    }
+}
+
+
+// MARK: TunneledAppDelegate implementation
+// See the protocol definition for details about the methods.
+// Note that we're excluding all the optional methods that we aren't using,
+// however your needs may be different.
+extension AppDelegate: TunneledAppDelegate {
+    func getPsiphonConfig() -> String? {
+        // In this example, we're going to retrieve our Psiphon config from a file in the app bundle.
+        // Alternatively, it could be a string literal in the code, or whatever makes sense.
+
+        guard let psiphonConfigUrl = Bundle.main.url(forResource: "psiphon-config", withExtension: "json") else {
+            NSLog("Error getting Psiphon config resource file URL!")
+            return nil
+        }
+
+        do {
+            return try String.init(contentsOf: psiphonConfigUrl)
+        } catch {
+            NSLog("Error reading Psiphon config resource file!")
+            return nil
+        }
+    }
+
+    /// Read the Psiphon embedded server entries resource file and return the contents.
+    /// * returns: The string of the contents of the file.
+    func getEmbeddedServerEntries() -> String? {
+        guard let psiphonEmbeddedServerEntriesUrl = Bundle.main.url(forResource: "psiphon-embedded-server-entries", withExtension: "txt") else {
+            NSLog("Error getting Psiphon embedded server entries resource file URL!")
+            return nil
+        }
+
+        do {
+            return try String.init(contentsOf: psiphonEmbeddedServerEntriesUrl)
+        } catch {
+            NSLog("Error reading Psiphon embedded server entries resource file!")
+            return nil
+        }
+    }
+
+    func onDiagnosticMessage(_ message: String) {
+        NSLog("onDiagnosticMessage: %@", message)
+    }
+
+    func onConnected() {
+        NSLog("onConnected")
+
+        DispatchQueue.main.sync {
+            let urlString = "https://freegeoip.net"
+            let url = URL.init(string: urlString)!
+            let mainView = self.window?.rootViewController as! ViewController
+            mainView.loadUrl(url)
+        }
+    }
+
+    func onListeningSocksProxyPort(_ port: Int) {
+        DispatchQueue.main.async {
+            JAHPAuthenticatingHTTPProtocol.resetSharedDemux()
+            self.socksProxyPort = port
+        }
+    }
+
+    func onListeningHttpProxyPort(_ port: Int) {
+        DispatchQueue.main.async {
+            JAHPAuthenticatingHTTPProtocol.resetSharedDemux()
+            self.httpProxyPort = port
+        }
+    }
+
+}

+ 158 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,158 @@
+{
+  "images" : [
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "NotificationIcon@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "iphone",
+      "filename" : "NotificationIcon@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-Small.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-Small@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "iphone",
+      "filename" : "Icon-Small@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "iphone",
+      "filename" : "Icon-40@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "57x57",
+      "idiom" : "iphone",
+      "filename" : "Icon.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "57x57",
+      "idiom" : "iphone",
+      "filename" : "Icon@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-60@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "Icon-60@3x.png",
+      "scale" : "3x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "NotificationIcon~ipad.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "20x20",
+      "idiom" : "ipad",
+      "filename" : "NotificationIcon~ipad@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-Small.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "29x29",
+      "idiom" : "ipad",
+      "filename" : "Icon-Small@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-40.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "40x40",
+      "idiom" : "ipad",
+      "filename" : "Icon-40@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "50x50",
+      "idiom" : "ipad",
+      "filename" : "Icon-Small-50.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "50x50",
+      "idiom" : "ipad",
+      "filename" : "Icon-Small-50@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "72x72",
+      "idiom" : "ipad",
+      "filename" : "Icon-72.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "72x72",
+      "idiom" : "ipad",
+      "filename" : "Icon-72@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-76.png",
+      "scale" : "1x"
+    },
+    {
+      "size" : "76x76",
+      "idiom" : "ipad",
+      "filename" : "Icon-76@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "83.5x83.5",
+      "idiom" : "ipad",
+      "filename" : "Icon-83.5@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "size" : "1024x1024",
+      "idiom" : "ios-marketing",
+      "filename" : "ios-marketing.png",
+      "scale" : "1x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-40.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-72.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-72@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-76.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small-50.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small-50@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/Icon@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/NotificationIcon@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/NotificationIcon@3x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/NotificationIcon~ipad@2x.png


binární
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/AppIcon.appiconset/ios-marketing.png


+ 6 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 27 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+                        <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

+ 42 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Base.lproj/Main.storyboard

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+    <device id="retina4_7" orientation="portrait">
+        <adaptation id="fullscreen"/>
+    </device>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="tne-QT-ifu">
+            <objects>
+                <viewController id="BYZ-38-t0r" customClass="ViewController" customModule="TunneledWebView" customModuleProvider="target" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
+                        <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <webView contentMode="scaleToFill" fixedFrame="YES" allowsInlineMediaPlayback="NO" mediaPlaybackRequiresUserAction="NO" mediaPlaybackAllowsAirPlay="NO" keyboardDisplayRequiresUserAction="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LFR-V1-aLA" userLabel="WebView">
+                                <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                                <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                                <color key="backgroundColor" red="0.36078431370000003" green="0.38823529410000002" blue="0.4039215686" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                <dataDetectorType key="dataDetectorTypes"/>
+                            </webView>
+                        </subviews>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                    <connections>
+                        <outlet property="webView" destination="LFR-V1-aLA" id="FRO-zc-IdT"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="133.59999999999999" y="113.79310344827587"/>
+        </scene>
+    </scenes>
+</document>

+ 59 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Info.plist

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+	<key>ITSAppUsesNonExemptEncryption</key>
+	<false/>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>NSAppTransportSecurity</key>
+	<dict>
+		<key>NSExceptionDomains</key>
+		<dict>
+			<key>ifconfig.co</key>
+			<string>YES</string>
+			<key>ip-api.com</key>
+			<string>YES</string>
+			<key>ipinfo.io</key>
+			<string>YES</string>
+		</dict>
+	</dict>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+	</array>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+</dict>
+</plist>

+ 14 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/TunneledWebView-Bridging-Header.h

@@ -0,0 +1,14 @@
+//
+//  TunneledWebView-Bridging-Header.h
+//  TunneledWebView
+//
+//  Created by Miro Kuratczyk on 2017-12-08.
+//  Copyright © 2017 Psiphon Inc. All rights reserved.
+//
+
+#ifndef TunneledWebView_Bridging_Header_h
+#define TunneledWebView_Bridging_Header_h
+
+#import "JAHPAuthenticatingHTTPProtocol.h"
+
+#endif /* TunneledWebView_Bridging_Header_h */

+ 33 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/ViewController.swift

@@ -0,0 +1,33 @@
+//
+//  ViewController.swift
+//  TunneledWebView
+//
+/*
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+
+import UIKit
+
+class ViewController: UIViewController {
+    
+    @IBOutlet var webView: UIWebView!
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        webView.isUserInteractionEnabled = true
+        webView.scrollView.isScrollEnabled = true
+    }
+
+    override func didReceiveMemoryWarning() {
+        super.didReceiveMemoryWarning()
+        // Dispose of any resources that can be recreated.
+    }
+
+    func loadUrl(_ url: URL) {
+        let request = URLRequest.init(url: url)
+        self.webView.loadRequest(request)
+    }
+}

+ 15 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/psiphon-config.json.stub

@@ -0,0 +1,15 @@
+/*
+These are the minimum values required to use the Psiphon Tunnel.
+ClientVersion is the version number of your software, which will be tracked in Psiphon stats.
+All other values will be provided to you by Psiphon Inc.
+*/
+
+{
+  "ClientVersion": "123", /* Must be a number in a string. */
+  "PropagationChannelId": "...",
+  "SponsorId": "...",
+  "RemoteServerListSignaturePublicKey": "...",
+  "RemoteServerListURLs": "[...]",
+  "ObfuscatedServerListRootURLs": "[...]",
+  "EmitDiagnosticNotices": true /* Useful when testing */
+}

+ 2 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/psiphon-embedded-server-entries.txt.stub

@@ -0,0 +1,2 @@
+Embedded server entries supplied by Psiphon Inc. go here.
+This file should be empty if embedded server entries are not being used. (But they should be used.)

+ 22 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebViewTests/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 36 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebViewTests/TunneledWebViewTests.swift

@@ -0,0 +1,36 @@
+//
+//  TunneledWebViewTests.swift
+//  TunneledWebViewTests
+//
+//  Created by Adam Pritchard on 2016-11-03.
+//  Copyright © 2016 Psiphon Inc. All rights reserved.
+//
+
+import XCTest
+@testable import TunneledWebView
+
+class TunneledWebViewTests: XCTestCase {
+    
+    override func setUp() {
+        super.setUp()
+        // Put setup code here. This method is called before the invocation of each test method in the class.
+    }
+    
+    override func tearDown() {
+        // Put teardown code here. This method is called after the invocation of each test method in the class.
+        super.tearDown()
+    }
+    
+    func testExample() {
+        // This is an example of a functional test case.
+        // Use XCTAssert and related functions to verify your tests produce the correct results.
+    }
+    
+    func testPerformanceExample() {
+        // This is an example of a performance test case.
+        self.measure {
+            // Put the code you want to measure the time of here.
+        }
+    }
+    
+}

+ 22 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebViewUITests/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 36 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebViewUITests/TunneledWebViewUITests.swift

@@ -0,0 +1,36 @@
+//
+//  TunneledWebViewUITests.swift
+//  TunneledWebViewUITests
+//
+//  Created by Adam Pritchard on 2016-11-03.
+//  Copyright © 2016 Psiphon Inc. All rights reserved.
+//
+
+import XCTest
+
+class TunneledWebViewUITests: XCTestCase {
+        
+    override func setUp() {
+        super.setUp()
+        
+        // Put setup code here. This method is called before the invocation of each test method in the class.
+        
+        // In UI tests it is usually best to stop immediately when a failure occurs.
+        continueAfterFailure = false
+        // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
+        XCUIApplication().launch()
+
+        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
+    }
+    
+    override func tearDown() {
+        // Put teardown code here. This method is called after the invocation of each test method in the class.
+        super.tearDown()
+    }
+    
+    func testExample() {
+        // Use recording to get started writing UI tests.
+        // Use XCTAssert and related functions to verify your tests produce the correct results.
+    }
+    
+}