Просмотр исходного кода

Perform OCSP requests manually

    - Require OCSP revocation check to pass
    - Perform OCSP requests manually through
      local HTTP proxy
mirokuratczyk 6 лет назад
Родитель
Сommit
d8d164c18f
23 измененных файлов с 1006 добавлено и 277 удалено
  1. 3 0
      MobileLibrary/iOS/SampleApps/.gitignore
  2. 62 0
      MobileLibrary/iOS/SampleApps/Common/AuthURLSessionTaskDelegate.h
  3. 146 0
      MobileLibrary/iOS/SampleApps/Common/AuthURLSessionTaskDelegate.m
  4. 30 0
      MobileLibrary/iOS/SampleApps/Common/OCSP.h
  5. 232 0
      MobileLibrary/iOS/SampleApps/Common/OCSP.m
  6. 24 0
      MobileLibrary/iOS/SampleApps/Common/URLEncode.h
  7. 39 0
      MobileLibrary/iOS/SampleApps/Common/URLEncode.m
  8. 1 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/Podfile
  9. 24 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/Podfile.lock
  10. 49 8
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.pbxproj
  11. 246 219
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift
  12. 6 3
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/Contents.json
  13. 0 2
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Info.plist
  14. 6 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/TunneledWebRequest-Bridging-Header.h
  15. 15 15
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/ViewController.swift
  16. 39 19
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m
  17. 1 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/Podfile
  18. 24 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/Podfile.lock
  19. 25 6
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/project.pbxproj
  20. 16 5
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/AppDelegate.swift
  21. 2 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Info.plist
  22. 1 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/TunneledWebView-Bridging-Header.h
  23. 15 0
      MobileLibrary/iOS/USAGE.md

+ 3 - 0
MobileLibrary/iOS/SampleApps/.gitignore

@@ -0,0 +1,3 @@
+# Cocoapods
+Pods
+*.xcworkspace

+ 62 - 0
MobileLibrary/iOS/SampleApps/Common/AuthURLSessionTaskDelegate.h

@@ -0,0 +1,62 @@
+//
+//  AuthURLSessionTaskDelegate.h
+//  TunneledWebRequest
+//
+/*
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+
+// NOTE: this file is shared by TunneledWebRequest and TunneledWebView
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/*
+ * AuthURLSessionTaskDelegate implements URLSession:task:didReceiveChallenge:completionHandler:
+ * of the NSURLSessionTaskDelegate protocol.
+ *
+ * The main motivation of AuthURLSessionTaskDelegate is to ensure that OCSP requests are not
+ * sent in plaintext outside of the tunnel.
+ *
+ * If the policy object for checking the revocation of certificates is created with
+ * SecPolicyCreateRevocation(kSecRevocationOCSPMethod | ...), and network access is allowed
+ * (the kSecRevocationNetworkAccessDisabled flag is not provided), a plaintext OCSP request over
+ * HTTP is triggered when SecTrustEvaluate() is called. This request does not respect NSURLProtocol
+ * subclassing.
+ *
+ * The solution is to inspect each X.509 certificate for the Online Certificate Status Protocol
+ * (1.3.6.1.5.5.7.48.1) Authority Information Access Method, which contains the locations (URLs) of
+ * the OCSP servers; then OCSP requests are then made to these servers through the local HTTP proxy.
+ *
+ * Note: The OCSP Authority Information Access Method is found in the Certificate Authority
+ *       Information Access (1.3.6.1.5.5.7.1.1) X.509v3 extension --
+ *       https://tools.ietf.org/html/rfc2459#section-4.2.2.1.
+ */
+@interface AuthURLSessionTaskDelegate : NSObject <NSURLSessionDelegate>
+
+/*
+ * Logger for errors.
+ */
+@property (nonatomic, strong) void (^logger)(NSString*);
+
+/*
+ * Local HTTP proxy port.
+ *
+ * OCSP request URL is constructed as:
+ *   http://127.0.0.1:<HTTP proxy port>/tunneled/<URL encoded OCSP request>
+ */
+@property (atomic, assign) NSInteger localHTTPProxyPort;
+
+- (id)initWithLogger:(void (^)(NSString*))logger andLocalHTTPProxyPort:(NSInteger)port;
+
+- (void)URLSession:(NSURLSession *)session
+              task:(NSURLSessionTask *)task
+didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 146 - 0
MobileLibrary/iOS/SampleApps/Common/AuthURLSessionTaskDelegate.m

@@ -0,0 +1,146 @@
+//
+//  AuthURLSessionTaskDelegate.m
+//  TunneledWebRequest
+//
+/*
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+
+// NOTE: this file is shared by TunneledWebRequest and TunneledWebView
+
+#import "AuthURLSessionTaskDelegate.h"
+
+#import "OCSP.h"
+#import "URLEncode.h"
+
+@implementation AuthURLSessionTaskDelegate
+
+- (id)initWithLogger:(void (^)(NSString*))logger andLocalHTTPProxyPort:(NSInteger)port{
+    self = [super init];
+
+    if (self) {
+        self.logger = logger;
+        self.localHTTPProxyPort = port;
+    }
+
+    return self;
+}
+
+- (void)logWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2) {
+    if (self.logger != nil) {
+        va_list arguments;
+        
+        va_start(arguments, format);
+        NSString *message = [[NSString alloc] initWithFormat:format arguments:arguments];
+        va_end(arguments);
+        
+        self.logger(message);
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session
+              task:(NSURLSessionTask *)task
+didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
+{
+#pragma unused(session)
+#pragma unused(task)
+    assert(challenge != nil);
+    assert(completionHandler != nil);
+    
+    // Resolve NSURLAuthenticationMethodServerTrust ourselves
+    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
+        [self logWithFormat:@"Got SSL certificate for %@, mainDocumentURL: %@, URL: %@",
+         challenge.protectionSpace.host,
+         [task.currentRequest mainDocumentURL],
+         [task.currentRequest URL]];
+        
+        SecTrustRef trust = challenge.protectionSpace.serverTrust;
+        if (trust == nil) {
+            assert(NO);
+        }
+        
+        SecPolicyRef policy = SecPolicyCreateRevocation(kSecRevocationOCSPMethod |
+                                                        kSecRevocationRequirePositiveResponse |
+                                                        kSecRevocationNetworkAccessDisabled);
+        SecTrustSetPolicies(trust, policy);
+        CFRelease(policy);
+        
+        NSError *e;
+        
+        NSArray <NSURL*>* ocspURLs = [OCSP ocspURLs:trust error:&e];
+        if (e != nil) {
+            [self logWithFormat:@"Error constructing OCSP URLs: %@", e.localizedDescription];
+            completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+            return;
+        }
+        
+        if ([ocspURLs count] == 0) {
+            [self logWithFormat:
+             @"Error no OCSP URLs in the Certificate Authority Information Access "
+             "(1.3.6.1.5.5.7.1.1) extension."];
+            completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+            return;
+        }
+        
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            for (NSURL *ocspURL in ocspURLs) {
+                
+                // The target URL must be encoded, so as to be valid within a query parameter.
+                NSString *encodedTargetUrl = [URLEncode encode:ocspURL.absoluteString];
+                
+                NSNumber *httpProxyPort = [NSNumber numberWithInt:
+                                           (int)self.localHTTPProxyPort];
+                
+                NSString *proxiedURLString = [NSString stringWithFormat:@"http://127.0.0.1:%@"
+                                              "/tunneled/%@",
+                                              httpProxyPort,
+                                              encodedTargetUrl];
+                NSURL *proxiedURL = [NSURL URLWithString:proxiedURLString];
+                if (proxiedURL == nil) {
+                    [self logWithFormat:@"Constructed invalid URL for OCSP request: %@",
+                                        proxiedURLString];
+                    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+                    return;
+                }
+                
+                NSURLRequest *ocspReq = [NSURLRequest requestWithURL:proxiedURL];
+
+                NSURLResponse *resp = nil;
+                NSError *e = nil;
+                NSData *data = [NSURLConnection sendSynchronousRequest:ocspReq
+                                                     returningResponse:&resp
+                                                                 error:&e];
+                if (e != nil) {
+                    [self logWithFormat:@"Error with OCSP request: %@", e.localizedDescription];
+                    continue;
+                }
+                
+                CFDataRef d = (__bridge CFDataRef)data;
+                SecTrustSetOCSPResponse(trust, d);
+                
+                SecTrustResultType trustResultType;
+                SecTrustEvaluate(trust, &trustResultType);
+
+                if (trustResultType == kSecTrustResultProceed || trustResultType == kSecTrustResultUnspecified) {
+                    NSURLCredential *credential = [NSURLCredential credentialForTrust:
+                                                   challenge.protectionSpace.serverTrust];
+                    assert(credential != nil);
+                    completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
+                    return;
+                }
+
+                completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+                return;
+            }
+        });
+
+        return;
+    }
+
+    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
+}
+
+@end

+ 30 - 0
MobileLibrary/iOS/SampleApps/Common/OCSP.h

@@ -0,0 +1,30 @@
+//
+//  OCSP.h
+//  TunneledWebRequest
+//
+/*
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+
+// NOTE: this file is shared by TunneledWebRequest and TunneledWebView
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface OCSP : NSObject
+
+/*
+ * Check in SecTrustRef (X.509 cert) for Online Certificate Status Protocol (1.3.6.1.5.5.7.48.1)
+ * authority information access method. This is found in the
+ * Certificate Authority Information Access (1.3.6.1.5.5.7.1.1) X.509v3 extension.
+ *
+ * X.509 Authority Information Access: https://tools.ietf.org/html/rfc2459#section-4.2.2.1
+ */
++ (NSArray<NSURL*>*_Nullable)ocspURLs:(SecTrustRef)secTrustRef error:(NSError**)error;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 232 - 0
MobileLibrary/iOS/SampleApps/Common/OCSP.m

@@ -0,0 +1,232 @@
+//
+//  OCSP.m
+//  TunneledWebRequest
+//
+/*
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+
+// NOTE: this file is shared by TunneledWebRequest and TunneledWebView
+
+#import "OCSP.h"
+#import "openssl/ocsp.h"
+#import "openssl/safestack.h"
+#import "openssl/x509.h"
+#import "openssl/x509v3.h"
+
+NSErrorDomain _Nonnull const OCSPErrorDomain = @"OCSPErrorDomain";
+
+typedef NS_ERROR_ENUM(OCSPErrorDomain, OCSPErrorCode) {
+    OCSPErrorCodeUnknown = -1,
+    OCSPErrorCodeInvalidNumCerts = 1,
+    OCSPErrorCodeNoLeafCert,
+    OCSPErrorCodeNoIssuerCert,
+    OCSPErrorCodeNoOCSPURLs,
+    OCSPErrorCodeEVPAllocFailed,
+    OCSPErrorCodeCertToIdFailed,
+    OCSPErrorCodeReqAllocFailed,
+    OCSPErrorCodeAddCertsToReqFailed,
+    OCSPErrorCodeFailedToSerializeOCSPReq,
+    OCSPErrorCodeConstructedInvalidURL
+};
+
+@implementation OCSP
+
++ (NSArray<NSURL*>*_Nullable)ocspURLs:(SecTrustRef)secTrustRef error:(NSError**)error {
+    
+    NSMutableArray <void(^)(void)> *cleanup = [[NSMutableArray alloc] init];
+    
+    CFIndex certificateCount = SecTrustGetCertificateCount(secTrustRef);
+    if (certificateCount < 2) {
+        NSString *errorString = [NSString stringWithFormat:@"Expected 2 or more certificates "
+                                                            "(at least leaf and issuer) in the "
+                                                            "trust chain but only found %ld",
+                                                           (long)certificateCount];
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeInvalidNumCerts
+                                 userInfo:@{NSLocalizedDescriptionKey:errorString}];
+        return nil;
+    }
+    
+    X509 *leaf = [OCSP certAtIndex:secTrustRef withIndex:0];
+    if (leaf == NULL) {
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeNoLeafCert
+                                 userInfo:@{NSLocalizedDescriptionKey:@"Failed to get leaf "
+                                                                       "certficate"}];
+        return nil;
+    }
+    
+    [cleanup addObject:^(){
+        X509_free(leaf);
+    }];
+    
+    X509 *issuer = [OCSP certAtIndex:secTrustRef withIndex:1];
+    if (issuer == NULL) {
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeNoIssuerCert
+                                 userInfo:@{NSLocalizedDescriptionKey:@"Failed to get issuer "
+                                                                       "certificate"}];
+        [OCSP execCleanupTasks:cleanup];
+        return nil;
+    }
+    
+    [cleanup addObject:^(){
+        X509_free(issuer);
+    }];
+    
+    NSArray<NSString*>* ocspURLs = [OCSP OCSPURLs:leaf];
+    if ([ocspURLs count] == 0) {
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeNoOCSPURLs
+                                 userInfo:@{NSLocalizedDescriptionKey:@"Found 0 OCSP URLs in leaf "
+                                                                       "certificate"}];
+        [OCSP execCleanupTasks:cleanup];
+        return nil;
+    }
+    
+    const EVP_MD *cert_id_md = EVP_sha1();
+    if (cert_id_md == NULL) {
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeEVPAllocFailed
+                                 userInfo:@{NSLocalizedDescriptionKey:@"Failed to allocate new EVP "
+                                                                       "sha1"}];
+        [OCSP execCleanupTasks:cleanup];
+        return nil;
+    }
+    
+    OCSP_CERTID *id_t = OCSP_cert_to_id(cert_id_md, leaf, issuer);
+    if (id_t == NULL) {
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeCertToIdFailed
+                                 userInfo:@{NSLocalizedDescriptionKey:@"Failed to create "
+                                                                       "OCSP_CERTID structure"}];
+        [OCSP execCleanupTasks:cleanup];
+        return nil;
+    }
+    
+    // Construct OCSP request
+    //
+    // https://www.ietf.org/rfc/rfc2560.txt
+    //
+    // An OCSP request using the GET method is constructed as follows:
+    //
+    // GET {url}/{url-encoding of base-64 encoding of the DER encoding of
+    //	   the OCSPRequest}
+    
+    OCSP_REQUEST *req = OCSP_REQUEST_new();
+    if (req == NULL) {
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeReqAllocFailed
+                                 userInfo:@{NSLocalizedDescriptionKey:@"Failed to allocate new "
+                                                                       "OCSP request"}];
+        [OCSP execCleanupTasks:cleanup];
+        return nil;
+    }
+    
+    [cleanup addObject:^(){
+        OCSP_REQUEST_free(req);
+    }];
+    
+    if (OCSP_request_add0_id(req, id_t) == NULL) {
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeAddCertsToReqFailed
+                                 userInfo:@{NSLocalizedDescriptionKey:@"Failed to add certs to "
+                                                                       "OCSP request"}];
+        [OCSP execCleanupTasks:cleanup];
+        return nil;
+    }
+    
+    unsigned char *ocspReq = NULL;
+    
+    int len = i2d_OCSP_REQUEST(req, &ocspReq);
+    
+    if (ocspReq == NULL) {
+        *error = [NSError errorWithDomain:OCSPErrorDomain
+                                     code:OCSPErrorCodeFailedToSerializeOCSPReq
+                                 userInfo:@{NSLocalizedDescriptionKey:@"Failed to serialize OCSP "
+                                                                       "request"}];
+        [OCSP execCleanupTasks:cleanup];
+        return nil;
+    }
+    
+    [cleanup addObject:^(){
+        free(ocspReq);
+    }];
+    
+    NSData *ocspReqData = [NSData dataWithBytes:ocspReq length:len];
+    NSString *encodedOCSPReqData = [ocspReqData base64EncodedStringWithOptions:kNilOptions];
+    NSString *escapedAndEncodedOCSPReqData = [encodedOCSPReqData
+                                              stringByAddingPercentEncodingWithAllowedCharacters:
+                                              NSCharacterSet.URLFragmentAllowedCharacterSet];
+    
+    NSMutableArray<NSURL*>* reqURLs = [[NSMutableArray alloc] initWithCapacity:[ocspURLs count]];
+    
+    for (NSString *ocspURL in ocspURLs) {
+        
+        NSString *reqURL = [NSString stringWithFormat:@"%@/%@",
+                                                      ocspURL,
+                                                      escapedAndEncodedOCSPReqData];
+        
+        NSURL *url = [NSURL URLWithString:reqURL];
+        if (url == nil) {
+            NSString *localizedDescription = [NSString stringWithFormat:@"Constructed invalid URL "
+                                                                         "for OCSP request: %@",
+                                                                        reqURL];
+            *error = [NSError errorWithDomain:OCSPErrorDomain
+                                         code:OCSPErrorCodeConstructedInvalidURL
+                                     userInfo:@{NSLocalizedDescriptionKey:localizedDescription}];
+            [OCSP execCleanupTasks:cleanup];
+            return nil;
+        }
+        
+        [reqURLs addObject:url];
+    }
+    
+    [OCSP execCleanupTasks:cleanup];
+    
+    return reqURLs;
+}
+
+#pragma mark - Internal Helpers
+
++ (X509*)certAtIndex:(SecTrustRef)trust withIndex:(int)index {
+    if (SecTrustGetCertificateCount(trust) < index) {
+        return nil;
+    }
+    
+    SecCertificateRef cert = SecTrustGetCertificateAtIndex(trust, index);
+    
+    NSData *data = (__bridge_transfer NSData *)SecCertificateCopyData(cert);
+    
+    const unsigned char *p = [data bytes];
+    X509 *x = d2i_X509(NULL, &p, [data length]);
+    
+    return x;
+}
+
++ (NSArray<NSString*>*)OCSPURLs:(X509*)x {
+    STACK_OF(OPENSSL_STRING) *ocspURLs = X509_get1_ocsp(x);
+    
+    NSMutableArray *URLs = [[NSMutableArray alloc]
+                            initWithCapacity:sk_OPENSSL_STRING_num(ocspURLs)];
+    
+    for (int i = 0; i < sk_OPENSSL_STRING_num(ocspURLs); i++) {
+        [URLs addObject:[NSString stringWithCString:sk_OPENSSL_STRING_value(ocspURLs, i)
+                                           encoding:NSUTF8StringEncoding]];
+    }
+    
+    sk_OPENSSL_STRING_free(ocspURLs);
+    
+    return URLs;
+}
+
++ (void)execCleanupTasks:(NSArray<void(^)(void)> *)cleanupTasks {
+    for (void (^cleanupTask)(void) in cleanupTasks) {
+        cleanupTask();
+    }
+}
+
+@end

+ 24 - 0
MobileLibrary/iOS/SampleApps/Common/URLEncode.h

@@ -0,0 +1,24 @@
+//
+//  URLEncode.h
+//  TunneledWebRequest
+//
+/*
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+
+// NOTE: this file is shared by TunneledWebRequest and TunneledWebView
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface URLEncode : NSObject
+
+// See comment in URLEncode.m
++ (NSString*__nullable)encode:(NSString*)url;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 39 - 0
MobileLibrary/iOS/SampleApps/Common/URLEncode.m

@@ -0,0 +1,39 @@
+//
+//  URLEncode.m
+//  TunneledWebRequest
+//
+/*
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+
+// NOTE: this file is shared by TunneledWebRequest and TunneledWebView
+
+#import "URLEncode.h"
+
+@implementation URLEncode
+
+// Encode all reserved characters. See: https://stackoverflow.com/a/34788364.
+//
+// From RFC 3986 (https://www.ietf.org/rfc/rfc3986.txt):
+//
+//   2.3.  Unreserved Characters
+//
+//   Characters that are allowed in a URI but do not have a reserved
+//   purpose are called unreserved.  These include uppercase and lowercase
+//   letters, decimal digits, hyphen, period, underscore, and tilde.
+//
+//   unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
++ (NSString*)encode:(NSString*)url {
+    NSCharacterSet *queryParamCharsAllowed = [NSCharacterSet
+                                              characterSetWithCharactersInString:
+                                              @"abcdefghijklmnopqrstuvwxyz"
+                                              "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+                                              "0123456789"
+                                              "-._~"];
+
+    return [url stringByAddingPercentEncodingWithAllowedCharacters:queryParamCharsAllowed];
+}
+
+@end

+ 1 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/Podfile

@@ -1,6 +1,7 @@
 platform :ios, '10.0'
 
 target 'TunneledWebRequest' do
+    pod 'OpenSSL-Universal', '1.0.2.17'
     pod 'PsiphonTunnel', :git => 'https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git'
 end
 

+ 24 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/Podfile.lock

@@ -0,0 +1,24 @@
+PODS:
+  - OpenSSL-Universal (1.0.2.17)
+  - PsiphonTunnel (2.0.2)
+
+DEPENDENCIES:
+  - OpenSSL-Universal (= 1.0.2.17)
+  - PsiphonTunnel (from `https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git`)
+
+EXTERNAL SOURCES:
+  PsiphonTunnel:
+    :git: https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git
+
+CHECKOUT OPTIONS:
+  PsiphonTunnel:
+    :commit: c9af3bab93637163e117de9d1e77435baa7646c0
+    :git: https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git
+
+SPEC CHECKSUMS:
+  OpenSSL-Universal: ff04c2e6befc3f1247ae039e60c93f76345b3b5a
+  PsiphonTunnel: 0c3f8677e4b26316beba57df78ed9c75634ce091
+
+PODFILE CHECKSUM: a17e050a23b29cf7dab9ff0e4bce879f3d239fac
+
+COCOAPODS: 1.4.0

+ 49 - 8
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.pbxproj

@@ -16,6 +16,9 @@
 		6626590E1DCB8CF400872F6C /* TunneledWebRequestUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6626590D1DCB8CF400872F6C /* TunneledWebRequestUITests.swift */; };
 		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 */; };
+		CEFA7ECE2294BB410078E41E /* OCSP.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFA7ECC2294BB410078E41E /* OCSP.m */; };
+		CEFA7ED12294BB860078E41E /* URLEncode.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFA7ECF2294BB860078E41E /* URLEncode.m */; };
+		CEFA7EDD2295A5530078E41E /* AuthURLSessionTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFA7EDB2295A5530078E41E /* AuthURLSessionTaskDelegate.m */; };
 		DEB1E38A2E15C48277D2E61E /* libPods-TunneledWebRequest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 37F726750F0082A9B17447DC /* libPods-TunneledWebRequest.a */; };
 /* End PBXBuildFile section */
 
@@ -51,8 +54,8 @@
 /* Begin PBXFileReference section */
 		37F726750F0082A9B17447DC /* libPods-TunneledWebRequest.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TunneledWebRequest.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 		662658EA1DCB8CF300872F6C /* TunneledWebRequest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TunneledWebRequest.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>"; };
+		662658ED1DCB8CF300872F6C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; usesTabs = 0; };
+		662658EF1DCB8CF300872F6C /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; usesTabs = 0; };
 		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>"; };
@@ -66,6 +69,13 @@
 		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>"; };
 		6BA09789075034B337A791DA /* Pods-TunneledWebRequest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TunneledWebRequest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TunneledWebRequest/Pods-TunneledWebRequest.debug.xcconfig"; sourceTree = "<group>"; };
+		CEFA7EB82294A9BB0078E41E /* TunneledWebRequest-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TunneledWebRequest-Bridging-Header.h"; sourceTree = "<group>"; };
+		CEFA7ECC2294BB410078E41E /* OCSP.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OCSP.m; path = ../../../Common/OCSP.m; sourceTree = "<group>"; };
+		CEFA7ECD2294BB410078E41E /* OCSP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OCSP.h; path = ../../../Common/OCSP.h; sourceTree = "<group>"; };
+		CEFA7ECF2294BB860078E41E /* URLEncode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = URLEncode.m; path = ../../Common/URLEncode.m; sourceTree = "<group>"; };
+		CEFA7ED02294BB860078E41E /* URLEncode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = URLEncode.h; path = ../../Common/URLEncode.h; sourceTree = "<group>"; };
+		CEFA7EDB2295A5530078E41E /* AuthURLSessionTaskDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AuthURLSessionTaskDelegate.m; path = ../../../Common/AuthURLSessionTaskDelegate.m; sourceTree = "<group>"; };
+		CEFA7EDC2295A5530078E41E /* AuthURLSessionTaskDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AuthURLSessionTaskDelegate.h; path = ../../../Common/AuthURLSessionTaskDelegate.h; sourceTree = "<group>"; };
 		FC00C7E45B17A3FDBDC1BF85 /* Pods-TunneledWebRequest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TunneledWebRequest.release.xcconfig"; path = "Pods/Target Support Files/Pods-TunneledWebRequest/Pods-TunneledWebRequest.release.xcconfig"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -131,12 +141,16 @@
 			children = (
 				662658ED1DCB8CF300872F6C /* AppDelegate.swift */,
 				662658EF1DCB8CF300872F6C /* ViewController.swift */,
+				CEFA7ED02294BB860078E41E /* URLEncode.h */,
+				CEFA7ECF2294BB860078E41E /* URLEncode.m */,
+				CEFA7EC22294B2D20078E41E /* URLSessionDelegate */,
 				662658F11DCB8CF300872F6C /* Main.storyboard */,
 				662658F41DCB8CF300872F6C /* Assets.xcassets */,
 				662658F61DCB8CF300872F6C /* LaunchScreen.storyboard */,
 				662658F91DCB8CF300872F6C /* Info.plist */,
 				6688DBB51DCD684B00721A9E /* psiphon-config.json */,
 				6682D90D1EB1334000329958 /* psiphon-embedded-server-entries.txt */,
+				CEFA7EB82294A9BB0078E41E /* TunneledWebRequest-Bridging-Header.h */,
 			);
 			path = TunneledWebRequest;
 			sourceTree = "<group>";
@@ -167,6 +181,17 @@
 			name = Frameworks;
 			sourceTree = "<group>";
 		};
+		CEFA7EC22294B2D20078E41E /* URLSessionDelegate */ = {
+			isa = PBXGroup;
+			children = (
+				CEFA7EDC2295A5530078E41E /* AuthURLSessionTaskDelegate.h */,
+				CEFA7EDB2295A5530078E41E /* AuthURLSessionTaskDelegate.m */,
+				CEFA7ECD2294BB410078E41E /* OCSP.h */,
+				CEFA7ECC2294BB410078E41E /* OCSP.m */,
+			);
+			path = URLSessionDelegate;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -240,6 +265,7 @@
 					662658E91DCB8CF300872F6C = {
 						CreatedOnToolsVersion = 8.0;
 						DevelopmentTeam = Q6HLNEX92A;
+						LastSwiftMigration = 1020;
 						ProvisioningStyle = Automatic;
 					};
 					662658FD1DCB8CF400872F6C = {
@@ -261,6 +287,7 @@
 			developmentRegion = English;
 			hasScannedForEncodings = 0;
 			knownRegions = (
+				English,
 				en,
 				Base,
 			);
@@ -364,8 +391,11 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CEFA7ECE2294BB410078E41E /* OCSP.m in Sources */,
 				662658F01DCB8CF300872F6C /* ViewController.swift in Sources */,
+				CEFA7EDD2295A5530078E41E /* AuthURLSessionTaskDelegate.m in Sources */,
 				662658EE1DCB8CF300872F6C /* AppDelegate.swift in Sources */,
+				CEFA7ED12294BB860078E41E /* URLEncode.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -474,6 +504,7 @@
 				SDKROOT = iphoneos;
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Debug;
@@ -524,6 +555,7 @@
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 				VALIDATE_PRODUCT = YES;
 			};
@@ -534,6 +566,7 @@
 			baseConfigurationReference = 6BA09789075034B337A791DA /* Pods-TunneledWebRequest.debug.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
 				DEVELOPMENT_TEAM = Q6HLNEX92A;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
@@ -545,7 +578,9 @@
 				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequest;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRIP_BITCODE_FROM_COPIED_FILES = NO;
-				SWIFT_VERSION = 3.0;
+				SWIFT_OBJC_BRIDGING_HEADER = "TunneledWebRequest/TunneledWebRequest-Bridging-Header.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
 			};
 			name = Debug;
 		};
@@ -554,6 +589,7 @@
 			baseConfigurationReference = FC00C7E45B17A3FDBDC1BF85 /* Pods-TunneledWebRequest.release.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
 				DEVELOPMENT_TEAM = Q6HLNEX92A;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
@@ -565,7 +601,8 @@
 				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequest;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRIP_BITCODE_FROM_COPIED_FILES = NO;
-				SWIFT_VERSION = 3.0;
+				SWIFT_OBJC_BRIDGING_HEADER = "TunneledWebRequest/TunneledWebRequest-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
 			};
 			name = Release;
 		};
@@ -575,11 +612,12 @@
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				ENABLE_BITCODE = YES;
 				INFOPLIST_FILE = TunneledWebRequestTests/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequestTests;
 				PRODUCT_NAME = "$(TARGET_NAME)";
-				SWIFT_VERSION = 3.0;
+				SWIFT_VERSION = 5.0;
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TunneledWebRequest.app/TunneledWebRequest";
 			};
 			name = Debug;
@@ -590,11 +628,12 @@
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				BUNDLE_LOADER = "$(TEST_HOST)";
 				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				ENABLE_BITCODE = YES;
 				INFOPLIST_FILE = TunneledWebRequestTests/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequestTests;
 				PRODUCT_NAME = "$(TARGET_NAME)";
-				SWIFT_VERSION = 3.0;
+				SWIFT_VERSION = 5.0;
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TunneledWebRequest.app/TunneledWebRequest";
 			};
 			name = Release;
@@ -604,11 +643,12 @@
 			buildSettings = {
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				ENABLE_BITCODE = YES;
 				INFOPLIST_FILE = TunneledWebRequestUITests/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequestUITests;
 				PRODUCT_NAME = "$(TARGET_NAME)";
-				SWIFT_VERSION = 3.0;
+				SWIFT_VERSION = 5.0;
 				TEST_TARGET_NAME = TunneledWebRequest;
 			};
 			name = Debug;
@@ -618,11 +658,12 @@
 			buildSettings = {
 				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
 				DEVELOPMENT_TEAM = Q6HLNEX92A;
+				ENABLE_BITCODE = YES;
 				INFOPLIST_FILE = TunneledWebRequestUITests/Info.plist;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.psiphon3.ios.TunneledWebRequestUITests;
 				PRODUCT_NAME = "$(TARGET_NAME)";
-				SWIFT_VERSION = 3.0;
+				SWIFT_VERSION = 5.0;
 				TEST_TARGET_NAME = TunneledWebRequest;
 			};
 			name = Release;

+ 246 - 219
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift

@@ -3,9 +3,9 @@
 //  TunneledWebRequest
 //
 /*
-Licensed under Creative Commons Zero (CC0).
-https://creativecommons.org/publicdomain/zero/1.0/
-*/
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
 
 import UIKit
 
@@ -16,14 +16,33 @@ import PsiphonTunnel
 class AppDelegate: UIResponder, UIApplicationDelegate {
 
     var window: UIWindow?
+    @objc public var socksProxyPort: Int = 0
+    @objc public var httpProxyPort: Int = 0
+
+    // The instance of PsiphonTunnel we'll use for connecting.
+    var psiphonTunnel: PsiphonTunnel?
+
+    // Delegate for handling certificate validation.
+    var authURLSessionTaskDelegate: AuthURLSessionTaskDelegate =
+        AuthURLSessionTaskDelegate.init(logger: {print("[AuthURLSessionTaskDelegate]: ", $0)},
+                                        andLocalHTTPProxyPort: 0)
+
+    @objc public 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!
+    }
 
-	// The instance of PsiphonTunnel we'll use for connecting.
-	var psiphonTunnel: PsiphonTunnel?
-
-    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+    internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
 
-		self.psiphonTunnel = PsiphonTunnel.newPsiphonTunnel(self)
+        self.psiphonTunnel = PsiphonTunnel.newPsiphonTunnel(self)
 
         return true
     }
@@ -45,183 +64,179 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
     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)")
-		}
+        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()
+        // Clean up the tunnel
+        NSLog("Stopping tunnel")
+        self.psiphonTunnel?.stop()
     }
 
-	/// Request URL using URLSession configured to use the current proxy.
-	/// * parameters:
-	///   - url: The URL to request.
-	///   - completion: A callback function that will received the string obtained
-	///     from the request, or nil if there's an error.
-	/// * returns: The string obtained from the request, or nil if there's an error.
-	func makeRequestViaUrlSessionProxy(_ url: String, completion: @escaping (_ result: String?) -> ()) {
-		let socksProxyPort = self.psiphonTunnel!.getLocalSocksProxyPort()
-		assert(socksProxyPort > 0)
-
-		let request = URLRequest(url: URL(string: url)!)
-
-		let config = URLSessionConfiguration.ephemeral
-		config.requestCachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
-		config.connectionProxyDictionary = [AnyHashable: Any]()
-
-		// Enable and set the SOCKS proxy values.
-		config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxy as String] = 1
-		config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyHost as String] = "127.0.0.1"
-		config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyPort as String] = socksProxyPort
-
-		// Alternatively, the HTTP proxy can be used. Below are the settings for that.
-		// The HTTPS key constants are mismatched and Xcode gives deprecation warnings, but they seem to be necessary to proxy HTTPS requests. This is probably a bug on Apple's side; see: https://forums.developer.apple.com/thread/19356#131446
-		// config.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = 1
-		// config.connectionProxyDictionary?[kCFNetworkProxiesHTTPProxy as String] = "127.0.0.1"
-		// config.connectionProxyDictionary?[kCFNetworkProxiesHTTPPort as String] = self.httpProxyPort
-		// config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyHost as String] = "127.0.0.1"
-		// config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyPort as String] = self.httpProxyPort
-
-		let session = URLSession.init(configuration: config, delegate: nil, delegateQueue: OperationQueue.current)
-
-		// Create the URLSession task that will make the request via the tunnel proxy.
-		let task = session.dataTask(with: request) {
-			(data: Data?, response: URLResponse?, error: Error?) in
-			if error != nil {
-				NSLog("Client-side error in request to \(url): \(String(describing: error))")
-				// Invoke the callback indicating error.
-				completion(nil)
-				return
-			}
-
-			if data == nil {
-				NSLog("Data from request to \(url) is nil")
-				// Invoke the callback indicating error.
-				completion(nil)
-				return
-			}
-
-			let httpResponse = response as? HTTPURLResponse
-			if httpResponse?.statusCode != 200 {
-				NSLog("Server-side error in request to \(url): \(String(describing: httpResponse))")
-				// Invoke the callback indicating error.
-				completion(nil)
-				return
-			}
-
-			let encodingName = response?.textEncodingName != nil ? response?.textEncodingName : "utf-8"
-			let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString!))
-
-			let stringData = String(data: data!, encoding: String.Encoding(rawValue: UInt(encoding)))
-
-			// Make sure the session is cleaned up.
-			session.invalidateAndCancel()
-
-			// Invoke the callback with the result.
-			completion(stringData)
-		}
-
-		// Start the request task.
-		task.resume()
-	}
-
-	/// Request URL using Psiphon's "URL proxy" mode.
-	/// For details, see the comment near the top of:
-	/// https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/master/psiphon/httpProxy.go
-	/// * parameters:
-	///   - url: The URL to request.
-	///   - completion: A callback function that will received the string obtained
-	///     from the request, or nil if there's an error.
-	/// * returns: The string obtained from the request, or nil if there's an error.
-	func makeRequestViaUrlProxy(_ url: String, completion: @escaping (_ result: String?) -> ()) {
-		let httpProxyPort = self.psiphonTunnel!.getLocalHttpProxyPort()
-		assert(httpProxyPort > 0)
-
-		// The target URL must be encoded so as to be valid within a query parameter.
-		// See this SO answer for why we're using this CharacterSet (and not using: https://stackoverflow.com/a/24888789
-		let queryParamCharsAllowed = CharacterSet.init(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~")
-
-		let encodedTargetURL = url.addingPercentEncoding(withAllowedCharacters: queryParamCharsAllowed)
-
-		let proxiedURL = "http://127.0.0.1:\(httpProxyPort)/tunneled/\(encodedTargetURL!)"
-
-		let task = URLSession.shared.dataTask(with: URL(string: proxiedURL)!) {
-			(data: Data?, response: URLResponse?, error: Error?) in
-			if error != nil {
-				NSLog("Client-side error in request to \(url): \(String(describing: error))")
-				// Invoke the callback indicating error.
-				completion(nil)
-				return
-			}
-
-			if data == nil {
-				NSLog("Data from request to \(url) is nil")
-				// Invoke the callback indicating error.
-				completion(nil)
-				return
-			}
-
-			let httpResponse = response as? HTTPURLResponse
-			if httpResponse?.statusCode != 200 {
-				NSLog("Server-side error in request to \(url): \(String(describing: httpResponse))")
-				// Invoke the callback indicating error.
-				completion(nil)
-				return
-			}
-
-			let encodingName = response?.textEncodingName != nil ? response?.textEncodingName : "utf-8"
-			let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString!))
-
-			let stringData = String(data: data!, encoding: String.Encoding(rawValue: UInt(encoding)))
-
-			// Invoke the callback with the result.
-			completion(stringData)
-		}
-
-		// Start the request task.
-		task.resume()
-	}
-}
+    /// Request URL using URLSession configured to use the current proxy.
+    /// * parameters:
+    ///   - url: The URL to request.
+    ///   - completion: A callback function that will received the string obtained
+    ///     from the request, or nil if there's an error.
+    /// * returns: The string obtained from the request, or nil if there's an error.
+    func makeRequestViaUrlSessionProxy(_ url: String, completion: @escaping (_ result: String?) -> ()) {
+        let socksProxyPort = self.socksProxyPort
+        assert(socksProxyPort > 0)
+
+        let request = URLRequest(url: URL(string: url)!)
+
+        let config = URLSessionConfiguration.ephemeral
+        config.requestCachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
+        config.connectionProxyDictionary = [AnyHashable: Any]()
+
+        // Enable and set the SOCKS proxy values.
+        config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxy as String] = 1
+        config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyHost as String] = "127.0.0.1"
+        config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyPort as String] = socksProxyPort
+
+        // Alternatively, the HTTP proxy can be used. Below are the settings for that.
+        // The HTTPS key constants are mismatched and Xcode gives deprecation warnings, but they seem to be necessary to proxy HTTPS requests. This is probably a bug on Apple's side; see: https://forums.developer.apple.com/thread/19356#131446
+        // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = 1
+        // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPProxy as String] = "127.0.0.1"
+        // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPPort as String] = self.httpProxyPort
+        // config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyHost as String] = "127.0.0.1"
+        // config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyPort as String] = self.httpProxyPort
+
+        let session = URLSession.init(configuration: config, delegate: authURLSessionTaskDelegate, delegateQueue: OperationQueue.current)
+
+        // Create the URLSession task that will make the request via the tunnel proxy.
+        let task = session.dataTask(with: request) {
+            (data: Data?, response: URLResponse?, error: Error?) in
+            if error != nil {
+                NSLog("Client-side error in request to \(url): \(String(describing: error))")
+                // Invoke the callback indicating error.
+                completion(nil)
+                return
+            }
+
+            if data == nil {
+                NSLog("Data from request to \(url) is nil")
+                // Invoke the callback indicating error.
+                completion(nil)
+                return
+            }
+
+            let httpResponse = response as? HTTPURLResponse
+            if httpResponse?.statusCode != 200 {
+                NSLog("Server-side error in request to \(url): \(String(describing: httpResponse))")
+                // Invoke the callback indicating error.
+                completion(nil)
+                return
+            }
+
+            let encodingName = response?.textEncodingName != nil ? response?.textEncodingName : "utf-8"
+            let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString?))
+
+            let stringData = String(data: data!, encoding: String.Encoding(rawValue: UInt(encoding)))
+
+            // Make sure the session is cleaned up.
+            session.invalidateAndCancel()
+
+            // Invoke the callback with the result.
+            completion(stringData)
+        }
 
+        // Start the request task.
+        task.resume()
+    }
+
+    /// Request URL using Psiphon's "URL proxy" mode.
+    /// For details, see the comment near the top of:
+    /// https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/master/psiphon/httpProxy.go
+    /// * parameters:
+    ///   - url: The URL to request.
+    ///   - completion: A callback function that will received the string obtained
+    ///     from the request, or nil if there's an error.
+    /// * returns: The string obtained from the request, or nil if there's an error.
+    func makeRequestViaUrlProxy(_ url: String, completion: @escaping (_ result: String?) -> ()) {
+        let httpProxyPort = self.httpProxyPort
+        assert(httpProxyPort > 0)
+
+        // The target URL must be encoded so as to be valid within a query parameter.
+        let encodedTargetURL = URLEncode.encode(url)
+
+        let proxiedURL = "http://127.0.0.1:\(httpProxyPort)/tunneled/\(encodedTargetURL!)"
+
+        let task = URLSession.shared.dataTask(with: URL(string: proxiedURL)!) {
+            (data: Data?, response: URLResponse?, error: Error?) in
+            if error != nil {
+                NSLog("Client-side error in request to \(url): \(String(describing: error))")
+                // Invoke the callback indicating error.
+                completion(nil)
+                return
+            }
+
+            if data == nil {
+                NSLog("Data from request to \(url) is nil")
+                // Invoke the callback indicating error.
+                completion(nil)
+                return
+            }
+
+            let httpResponse = response as? HTTPURLResponse
+            if httpResponse?.statusCode != 200 {
+                NSLog("Server-side error in request to \(url): \(String(describing: httpResponse))")
+                // Invoke the callback indicating error.
+                completion(nil)
+                return
+            }
+
+            let encodingName = response?.textEncodingName != nil ? response?.textEncodingName : "utf-8"
+            let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString?))
+
+            let stringData = String(data: data!, encoding: String.Encoding(rawValue: UInt(encoding)))
+
+            // Invoke the callback with the result.
+            completion(stringData)
+        }
+
+        // Start the request task.
+        task.resume()
+    }
+}
 
 // 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() -> Any? {
-		// 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
-		}
-	}
+    func getPsiphonConfig() -> Any? {
+        // 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.
@@ -243,61 +258,73 @@ extension AppDelegate: TunneledAppDelegate {
         NSLog("onDiagnosticMessage(%@): %@", timestamp, message)
     }
 
-	func onConnected() {
-		NSLog("onConnected")
+    func onConnected() {
+        NSLog("onConnected")
 
-		// After we're connected, make tunneled requests and populate the webview.
+        // After we're connected, make tunneled requests and populate the webview.
 
-		DispatchQueue.global(qos: .default).async {
-			// First we'll make a "what is my IP" request via makeRequestViaUrlSessionProxy().
-			let url = "https://freegeoip.app/json/"
-			self.makeRequestViaUrlSessionProxy(url) {
-				(_ result: String?) in
+        DispatchQueue.global(qos: .default).async {
+            // First we'll make a "what is my IP" request via makeRequestViaUrlSessionProxy().
+            let url = "https://freegeoip.app/json/"
+            self.makeRequestViaUrlSessionProxy(url) {
+                (_ result: String?) in
 
-				if result == nil {
-					NSLog("Failed to get \(url)")
-					return
-				}
+                if result == nil {
+                    NSLog("Failed to get \(url)")
+                    return
+                }
 
-				// Do a little pretty-printing.
-				let prettyResult = result?.replacingOccurrences(of: ",", with: ",\n  ")
-					.replacingOccurrences(of: "{", with: "{\n  ")
-					.replacingOccurrences(of: "}", with: "\n}")
+                // Do a little pretty-printing.
+                let prettyResult = result?.replacingOccurrences(of: ",", with: ",\n  ")
+                    .replacingOccurrences(of: "{", with: "{\n  ")
+                    .replacingOccurrences(of: "}", with: "\n}")
 
-				DispatchQueue.main.sync {
-					// Load the result into the view.
+                DispatchQueue.main.sync {
+                    // Load the result into the view.
                     let mainView = self.window?.rootViewController as! ViewController
-					mainView.appendToView("Result from \(url):\n\(prettyResult!)")
-				}
-
-				// Then we'll make a different "what is my IP" request via makeRequestViaUrlProxy().
-				DispatchQueue.global(qos: .default).async {
-					let url = "https://ifconfig.co/json"
-					self.makeRequestViaUrlProxy(url) {
-						(_ result: String?) in
-
-						if result == nil {
-							NSLog("Failed to get \(url)")
-							return
-						}
-
-						// Do a little pretty-printing.
-						let prettyResult = result?.replacingOccurrences(of: ",", with: ",\n  ")
-							.replacingOccurrences(of: "{", with: "{\n  ")
-							.replacingOccurrences(of: "}", with: "\n}")
-
-						DispatchQueue.main.sync {
-							// Load the result into the view.
+                    mainView.appendToView("Result from \(url):\n\(prettyResult!)")
+                }
+
+                // Then we'll make a different "what is my IP" request via makeRequestViaUrlProxy().
+                DispatchQueue.global(qos: .default).async {
+                    let url = "https://ifconfig.co/json"
+                    self.makeRequestViaUrlProxy(url) {
+                        (_ result: String?) in
+
+                        if result == nil {
+                            NSLog("Failed to get \(url)")
+                            return
+                        }
+
+                        // Do a little pretty-printing.
+                        let prettyResult = result?.replacingOccurrences(of: ",", with: ",\n  ")
+                            .replacingOccurrences(of: "{", with: "{\n  ")
+                            .replacingOccurrences(of: "}", with: "\n}")
+
+                        DispatchQueue.main.sync {
+                            // Load the result into the view.
                             let mainView = self.window?.rootViewController as! ViewController
-							mainView.appendToView("Result from \(url):\n\(prettyResult!)")
-						}
+                            mainView.appendToView("Result from \(url):\n\(prettyResult!)")
+                        }
+
+                        // We'll leave the tunnel open for when we want to make
+                        // more requests. It will get stopped by `applicationWillTerminate`.
+                    }
+                }
+            }
+        }
+    }
 
-						// We'll leave the tunnel open for when we want to make
-						// more requests. It will get stopped by `applicationWillTerminate`.
-					}
-				}
+    func onListeningSocksProxyPort(_ port: Int) {
+        DispatchQueue.main.async {
+            self.socksProxyPort = port
+        }
+    }
 
-			}
-		}
-	}
+    func onListeningHttpProxyPort(_ port: Int) {
+        DispatchQueue.main.async {
+            self.authURLSessionTaskDelegate.localHTTPProxyPort = port
+            self.httpProxyPort = port
+        }
+    }
 }

+ 6 - 3
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -101,12 +101,15 @@
       "idiom" : "ipad",
       "filename" : "ipad-pro-app-83.5pt@2x.png",
       "scale" : "2x"
+    },
+    {
+      "idiom" : "ios-marketing",
+      "size" : "1024x1024",
+      "scale" : "1x"
     }
   ],
   "info" : {
     "version" : 1,
     "author" : "xcode"
   }
-}
-
-
+}

+ 0 - 2
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Info.plist

@@ -26,8 +26,6 @@
 	<dict>
 		<key>NSExceptionDomains</key>
 		<dict>
-			<key>ifconfig.co</key>
-			<string>YES</string>
 			<key>ip-api.com</key>
 			<string>YES</string>
 			<key>ipinfo.io</key>

+ 6 - 0
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/TunneledWebRequest-Bridging-Header.h

@@ -0,0 +1,6 @@
+//
+//  Use this file to import your target's public headers that you would like to expose to Swift.
+//
+
+#import "AuthURLSessionTaskDelegate.h"
+#import "URLEncode.h"

+ 15 - 15
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/ViewController.swift

@@ -3,40 +3,40 @@
 //  TunneledWebView
 //
 /*
-Licensed under Creative Commons Zero (CC0).
-https://creativecommons.org/publicdomain/zero/1.0/
-*/
+ Licensed under Creative Commons Zero (CC0).
+ https://creativecommons.org/publicdomain/zero/1.0/
+ */
 
 
 import UIKit
 
 class ViewController: UIViewController {
-    
-	@IBOutlet var webView: UIWebView!
 
-	var viewText: String = ""
-	
+    @IBOutlet var webView: UIWebView!
+
+    var viewText: String = ""
+
     override func viewDidLoad() {
         super.viewDidLoad()
 
-		webView.isUserInteractionEnabled = true
-		webView.scrollView.isScrollEnabled = true
-	}
+        webView.isUserInteractionEnabled = true
+        webView.scrollView.isScrollEnabled = true
+    }
 
     override func didReceiveMemoryWarning() {
         super.didReceiveMemoryWarning()
         // Dispose of any resources that can be recreated.
     }
-    
+
     func appendToView(_ text: String) {
         let escapedText = text.replacingOccurrences(of: "\r", with: "")
 
-		self.viewText += "\n\n"
-		self.viewText += escapedText
+        self.viewText += "\n\n"
+        self.viewText += escapedText
 
-		let html = "<pre>" + self.viewText + "</pre>"
+        let html = "<pre>" + self.viewText + "</pre>"
 
-		self.webView.loadHTMLString(html, baseURL: nil)
+        self.webView.loadHTMLString(html, baseURL: nil)
     }
 
 }

+ 39 - 19
MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m

@@ -51,7 +51,10 @@
 #import "JAHPCacheStoragePolicy.h"
 #import "JAHPQNSURLSessionDemux.h"
 
+#import "AuthURLSessionTaskDelegate.h"
+#import "OCSP.h"
 #import "TunneledWebView-Swift.h"
+#import "URLEncode.h"
 
 // I use the following typedef to keep myself sane in the face of the wacky
 // Objective-C block syntax.
@@ -171,6 +174,7 @@ static JAHPQNSURLSessionDemux *sharedDemuxInstance = nil;
 
             // Set proxy
             NSString* proxyHost = @"localhost";
+
             NSNumber* socksProxyPort = [NSNumber numberWithInt: (int)[AppDelegate sharedDelegate].socksProxyPort];
             NSNumber* httpProxyPort = [NSNumber numberWithInt: (int)[AppDelegate sharedDelegate].httpProxyPort];
 
@@ -303,7 +307,7 @@ static NSString * kJAHPRecursiveRequestFlagProperty = @"com.jivesoftware.JAHPAut
             [self authenticatingHTTPProtocol:nil logWithFormat:@"accept request %@", url];
         }
     }
-    
+
     return shouldAccept;
 }
 
@@ -628,11 +632,11 @@ static NSString * kJAHPRecursiveRequestFlagProperty = @"com.jivesoftware.JAHPAut
             //
             // [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge not cancelled; no challenge pending"];
         } else {
-            id<JAHPAuthenticatingHTTPProtocolDelegate>  strongeDelegate;
+            id<JAHPAuthenticatingHTTPProtocolDelegate>  strongDelegate;
             NSURLAuthenticationChallenge *  challenge;
             JAHPDidCancelAuthenticationChallengeHandler  didCancelAuthenticationChallengeHandler;
             
-            strongeDelegate = [[self class] delegate];
+            strongDelegate = [[self class] delegate];
             
             challenge = self.pendingChallenge;
             didCancelAuthenticationChallengeHandler = self.pendingDidCancelAuthenticationChallengeHandler;
@@ -640,12 +644,12 @@ static NSString * kJAHPRecursiveRequestFlagProperty = @"com.jivesoftware.JAHPAut
             self.pendingChallengeCompletionHandler = nil;
             self.pendingDidCancelAuthenticationChallengeHandler = nil;
             
-            if ([strongeDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:didCancelAuthenticationChallenge:)]) {
+            if ([strongDelegate 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];
+                [strongDelegate authenticatingHTTPProtocol:self didCancelAuthenticationChallenge:challenge];
             } else if (didCancelAuthenticationChallengeHandler) {
                 didCancelAuthenticationChallengeHandler(self, challenge);
             } else {
@@ -766,7 +770,10 @@ static NSString * kJAHPRecursiveRequestFlagProperty = @"com.jivesoftware.JAHPAut
     [[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
+- (void)URLSession:(NSURLSession *)session
+              task:(NSURLSessionTask *)task
+didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
+ completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
 {
     // rdar://21484589
     // this is called from JAHPQNSURLSessionDemuxTaskInfo,
@@ -775,37 +782,50 @@ static NSString * kJAHPRecursiveRequestFlagProperty = @"com.jivesoftware.JAHPAut
     // 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;
-    
+    id<JAHPAuthenticatingHTTPProtocolDelegate> strongDelegate;
+
 #pragma unused(session)
 #pragma unused(task)
     assert(task == self.task);
     assert(challenge != nil);
     assert(completionHandler != nil);
     assert([NSThread currentThread] == self.clientThread);
-    
+
+    // Resolve NSURLAuthenticationMethodServerTrust ourselves
+    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
+        // Delegate for handling certificate validation.
+        // Makes OCSP requests through local HTTP proxy.
+        AuthURLSessionTaskDelegate *authHandler = [[AppDelegate sharedDelegate] authURLSessionTaskDelegate];
+        
+        [authHandler URLSession:session
+                                task:task
+                 didReceiveChallenge:challenge
+                   completionHandler:completionHandler];
+        return;
+    }
+
     // 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];
-    
+
+    strongDelegate = [[self class] delegate];
+
     result = NO;
-    if ([strongeDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace:)]) {
-        result = [strongeDelegate authenticatingHTTPProtocol:self canAuthenticateAgainstProtectionSpace:[challenge protectionSpace]];
+    if ([strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace:)]) {
+        result = [strongDelegate 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);
     }
 }

+ 1 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/Podfile

@@ -1,6 +1,7 @@
 platform :ios, '10.0'
 
 target 'TunneledWebView' do
+    pod 'OpenSSL-Universal', '1.0.2.17'
     pod 'PsiphonTunnel', :git => 'https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git'
 end
 

+ 24 - 0
MobileLibrary/iOS/SampleApps/TunneledWebView/Podfile.lock

@@ -0,0 +1,24 @@
+PODS:
+  - OpenSSL-Universal (1.0.2.17)
+  - PsiphonTunnel (2.0.2)
+
+DEPENDENCIES:
+  - OpenSSL-Universal (= 1.0.2.17)
+  - PsiphonTunnel (from `https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git`)
+
+EXTERNAL SOURCES:
+  PsiphonTunnel:
+    :git: https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git
+
+CHECKOUT OPTIONS:
+  PsiphonTunnel:
+    :commit: c9af3bab93637163e117de9d1e77435baa7646c0
+    :git: https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git
+
+SPEC CHECKSUMS:
+  OpenSSL-Universal: ff04c2e6befc3f1247ae039e60c93f76345b3b5a
+  PsiphonTunnel: 0c3f8677e4b26316beba57df78ed9c75634ce091
+
+PODFILE CHECKSUM: 96ed660616525565a4c1e99a04e1c72a9474883d
+
+COCOAPODS: 1.4.0

+ 25 - 6
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/project.pbxproj

@@ -21,6 +21,9 @@
 		6626590E1DCB8CF400872F6C /* TunneledWebViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6626590D1DCB8CF400872F6C /* TunneledWebViewUITests.swift */; };
 		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 */; };
+		CEFA7ECB2294BB080078E41E /* OCSP.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFA7ECA2294BB080078E41E /* OCSP.m */; };
+		CEFA7ED42294BB8F0078E41E /* URLEncode.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFA7ED22294BB8F0078E41E /* URLEncode.m */; };
+		CEFA7ED72294BC0C0078E41E /* AuthURLSessionTaskDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFA7ED62294BC0C0078E41E /* AuthURLSessionTaskDelegate.m */; };
 		DDFD23795085E5852A8F4DD5 /* libPods-TunneledWebView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E472F80E34E361EB72B2FD0C /* libPods-TunneledWebView.a */; };
 /* End PBXBuildFile section */
 
@@ -81,6 +84,12 @@
 		6688DBB51DCD684B00721A9E /* psiphon-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "psiphon-config.json"; sourceTree = "<group>"; };
 		76C8CF5D2CF9F4228B9CD56E /* Pods-TunneledWebView.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TunneledWebView.release.xcconfig"; path = "Pods/Target Support Files/Pods-TunneledWebView/Pods-TunneledWebView.release.xcconfig"; sourceTree = "<group>"; };
 		85795C6590EED64B7A6684AA /* Pods-TunneledWebView.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TunneledWebView.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TunneledWebView/Pods-TunneledWebView.debug.xcconfig"; sourceTree = "<group>"; };
+		CEFA7EC92294BB080078E41E /* OCSP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OCSP.h; path = ../../../Common/OCSP.h; sourceTree = "<group>"; };
+		CEFA7ECA2294BB080078E41E /* OCSP.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OCSP.m; path = ../../../Common/OCSP.m; sourceTree = "<group>"; };
+		CEFA7ED22294BB8F0078E41E /* URLEncode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = URLEncode.m; path = ../../../Common/URLEncode.m; sourceTree = "<group>"; };
+		CEFA7ED32294BB8F0078E41E /* URLEncode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = URLEncode.h; path = ../../../Common/URLEncode.h; sourceTree = "<group>"; };
+		CEFA7ED52294BC0C0078E41E /* AuthURLSessionTaskDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AuthURLSessionTaskDelegate.h; path = ../../../Common/AuthURLSessionTaskDelegate.h; sourceTree = "<group>"; };
+		CEFA7ED62294BC0C0078E41E /* AuthURLSessionTaskDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AuthURLSessionTaskDelegate.m; path = ../../../Common/AuthURLSessionTaskDelegate.m; sourceTree = "<group>"; };
 		E472F80E34E361EB72B2FD0C /* libPods-TunneledWebView.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TunneledWebView.a"; sourceTree = BUILT_PRODUCTS_DIR; };
 /* End PBXFileReference section */
 
@@ -113,6 +122,8 @@
 		4E0CA95F1FDE554B00B48BCA /* JiveAuthenticatingHTTPProtocol */ = {
 			isa = PBXGroup;
 			children = (
+				CEFA7ED52294BC0C0078E41E /* AuthURLSessionTaskDelegate.h */,
+				CEFA7ED62294BC0C0078E41E /* AuthURLSessionTaskDelegate.m */,
 				4E0CA9601FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.h */,
 				4E0CA9611FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.m */,
 				4E0CA9621FDE554B00B48BCA /* JAHPCacheStoragePolicy.h */,
@@ -121,6 +132,10 @@
 				4E0CA9651FDE554B00B48BCA /* JAHPCanonicalRequest.m */,
 				4E0CA9661FDE554B00B48BCA /* JAHPQNSURLSessionDemux.h */,
 				4E0CA9671FDE554B00B48BCA /* JAHPQNSURLSessionDemux.m */,
+				CEFA7EC92294BB080078E41E /* OCSP.h */,
+				CEFA7ECA2294BB080078E41E /* OCSP.m */,
+				CEFA7ED32294BB8F0078E41E /* URLEncode.h */,
+				CEFA7ED22294BB8F0078E41E /* URLEncode.m */,
 			);
 			name = JiveAuthenticatingHTTPProtocol;
 			path = External/JiveAuthenticatingHTTPProtocol;
@@ -295,6 +310,7 @@
 			developmentRegion = English;
 			hasScannedForEncodings = 0;
 			knownRegions = (
+				English,
 				en,
 				Base,
 			);
@@ -404,7 +420,10 @@
 				4E0CA96B1FDE554B00B48BCA /* JAHPQNSURLSessionDemux.m in Sources */,
 				662658EE1DCB8CF300872F6C /* AppDelegate.swift in Sources */,
 				4E0CA9681FDE554B00B48BCA /* JAHPAuthenticatingHTTPProtocol.m in Sources */,
+				CEFA7ED72294BC0C0078E41E /* AuthURLSessionTaskDelegate.m in Sources */,
 				4E0CA9691FDE554B00B48BCA /* JAHPCacheStoragePolicy.m in Sources */,
+				CEFA7ECB2294BB080078E41E /* OCSP.m in Sources */,
+				CEFA7ED42294BB8F0078E41E /* URLEncode.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -586,7 +605,7 @@
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRIP_BITCODE_FROM_COPIED_FILES = NO;
 				SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/TunneledWebView/TunneledWebView-Bridging-Header.h";
-				SWIFT_VERSION = 3.0;
+				SWIFT_VERSION = 5.0;
 			};
 			name = Debug;
 		};
@@ -608,7 +627,7 @@
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRIP_BITCODE_FROM_COPIED_FILES = NO;
 				SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/TunneledWebView/TunneledWebView-Bridging-Header.h";
-				SWIFT_VERSION = 3.0;
+				SWIFT_VERSION = 5.0;
 			};
 			name = Release;
 		};
@@ -622,7 +641,7 @@
 				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;
+				SWIFT_VERSION = 5.0;
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TunneledWebView.app/TunneledWebView";
 			};
 			name = Debug;
@@ -637,7 +656,7 @@
 				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;
+				SWIFT_VERSION = 5.0;
 				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TunneledWebView.app/TunneledWebView";
 			};
 			name = Release;
@@ -651,7 +670,7 @@
 				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;
+				SWIFT_VERSION = 5.0;
 				TEST_TARGET_NAME = TunneledWebView;
 			};
 			name = Debug;
@@ -665,7 +684,7 @@
 				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;
+				SWIFT_VERSION = 5.0;
 				TEST_TARGET_NAME = TunneledWebView;
 			};
 			name = Release;

+ 16 - 5
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/AppDelegate.swift

@@ -13,16 +13,21 @@ import PsiphonTunnel
 
 
 @UIApplicationMain
-@objc class AppDelegate: UIResponder, UIApplicationDelegate, JAHPAuthenticatingHTTPProtocolDelegate {
+@objc class AppDelegate: UIResponder, UIApplicationDelegate {
 
     var window: UIWindow?
-    var socksProxyPort: Int = 0
-    var httpProxyPort: Int = 0
+    @objc public var socksProxyPort: Int = 0
+    @objc public var httpProxyPort: Int = 0
 
     // The instance of PsiphonTunnel we'll use for connecting.
     var psiphonTunnel: PsiphonTunnel?
 
-    class func sharedDelegate() -> AppDelegate {
+    // Delegate for handling certificate validation.
+    @objc public var authURLSessionTaskDelegate: AuthURLSessionTaskDelegate =
+        AuthURLSessionTaskDelegate.init(logger: {print("[AuthURLSessionTaskDelegate]: ", $0)},
+                                         andLocalHTTPProxyPort: 0)
+    
+    @objc public class func sharedDelegate() -> AppDelegate {
         var delegate: AppDelegate?
         if (Thread.isMainThread) {
             delegate = UIApplication.shared.delegate as? AppDelegate
@@ -34,7 +39,7 @@ import PsiphonTunnel
         return delegate!
     }
 
-    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
+    internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
 
         // Set the class delegate and register NSURL subclass
@@ -110,6 +115,11 @@ import PsiphonTunnel
     }
 }
 
+extension AppDelegate: JAHPAuthenticatingHTTPProtocolDelegate {
+    func authenticatingHTTPProtocol(_ authenticatingHTTPProtocol: JAHPAuthenticatingHTTPProtocol?, logMessage message: String) {
+        NSLog("[JAHPAuthenticatingHTTPProtocol] %@", message)
+    }
+}
 
 // MARK: TunneledAppDelegate implementation
 // See the protocol definition for details about the methods.
@@ -173,6 +183,7 @@ extension AppDelegate: TunneledAppDelegate {
 
     func onListeningHttpProxyPort(_ port: Int) {
         DispatchQueue.main.async {
+            self.authURLSessionTaskDelegate.localHTTPProxyPort = port
             JAHPAuthenticatingHTTPProtocol.resetSharedDemux()
             self.httpProxyPort = port
         }

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

@@ -24,6 +24,8 @@
 	<true/>
 	<key>NSAppTransportSecurity</key>
 	<dict>
+		<key>NSAllowsArbitraryLoads</key>
+		<false/>
 		<key>NSExceptionDomains</key>
 		<dict>
 			<key>ifconfig.co</key>

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

@@ -9,6 +9,7 @@
 #ifndef TunneledWebView_Bridging_Header_h
 #define TunneledWebView_Bridging_Header_h
 
+#import "AuthURLSessionTaskDelegate.h"
 #import "JAHPAuthenticatingHTTPProtocol.h"
 
 #endif /* TunneledWebView_Bridging_Header_h */

+ 15 - 0
MobileLibrary/iOS/USAGE.md

@@ -48,10 +48,25 @@ The following architecture targets are compiled into the Library's framework bin
 
 When run in a simulator, there may be errors shown in the device log. This does not seem to affect the execution of the app (or Library).
 
+
+## Online Certificate Status Protocol (OCSP) Leaks
+
+### Background
+
+On iOS, requests which use HTTPS can trigger remote certificate revocation checks. Currently, the OS does this automatically by making plaintext HTTP [OCSP requests](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol).
+
+Unfortunately, these OCSP requests do not respect [connection proxy dictionary settings](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration/1411499-connectionproxydictionary?language=objc) or [NSURLProtocol](https://developer.apple.com/documentation/foundation/nsurlprotocol) subclasses; they are likely performed out of process. The payload in the plaintext OCSP requests leaks the identity of the certificate that is being validated. The risk is that an observer can [map the certificate's serial number back to the certificate](https://github.com/OnionBrowser/OnionBrowser/issues/178#issue-437802301) to find more information about the website or server being accessed.
+
+### Fix
+
+A fix has been implemented in both sample apps: [TunneledWebRequest](SampleApps/TunneledWebRequest) and [TunneledWebView](SampleApps/TunneledWebView). This is done by implementing [URLSession:task:didReceiveChallenge:completionHandler:](https://developer.apple.com/documentation/foundation/nsurlsessiontaskdelegate/1411595-urlsession?language=objc) of the [NSURLSessionTaskDelegate](https://developer.apple.com/documentation/foundation/nsurlsessiontaskdelegate) protocol. This allows us to perform OCSP requests manually and ensure that they are tunneled. See the comments in [SampleApps/Common/AuthURLSessionTaskDelegate.h](SampleApps/Common/AuthURLSessionTaskDelegate.h) and both sample apps for a reference implementation.
+
 ## Proxying a web view
 
 `WKWebView` _cannot_ be proxied. `UIWebView` _can_ be. Some [googling](https://www.google.ca/search?q=uiwebview+nsurlprotocol+proxy) should provide many example of how to do this. Here is some extensive information for [Objective-C](https://www.raywenderlich.com/59982/nsurlprotocol-tutorial) and [Swift](https://www.raywenderlich.com/76735/using-nsurlprotocol-swift).
 
+We have provided a reference implementation for proxying `UIWebView` in [TunneledWebView](SampleApps/TunneledWebView). The shortcomings of this implementation are discussed in [SampleApps/TunneledWebView/README.md](SampleApps/TunneledWebView/README.md#-caveats-).
+
 ## Other notes
 
 If you encounter an app crash due to `SIGPIPE`, please let us know. This occurs in the debugger, but it's not clear if it happens in a production app (or is a problem). If you encounter a `SIGPIPE` breakpoint while running under the debugger, follow [these instructions](https://plus.google.com/113241179738681655641/posts/BmMiY8mpsB7) to disable it.