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

Merge branch 'master' into exchange

Rod Hynes 6 лет назад
Родитель
Сommit
d318c8d93b
28 измененных файлов с 613 добавлено и 324 удалено
  1. 3 0
      MobileLibrary/iOS/SampleApps/.gitignore
  2. 3 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/Podfile
  3. 37 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/Podfile.lock
  4. 23 8
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest.xcodeproj/project.pbxproj
  5. 310 228
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/AppDelegate.swift
  6. 6 3
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Assets.xcassets/AppIcon.appiconset/Contents.json
  7. 4 2
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/Info.plist
  8. 6 0
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/TunneledWebRequest-Bridging-Header.h
  9. 15 15
      MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/ViewController.swift
  10. 46 19
      MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m
  11. 3 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/Podfile
  12. 37 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/Podfile.lock
  13. 7 6
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView.xcodeproj/project.pbxproj
  14. 29 6
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/AppDelegate.swift
  15. 4 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/Info.plist
  16. 2 0
      MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/TunneledWebView-Bridging-Header.h
  17. 22 0
      MobileLibrary/iOS/USAGE.md
  18. 18 17
      psiphon/common/protocol/protocol.go
  19. 2 2
      psiphon/common/protocol/serverEntry.go
  20. 14 9
      psiphon/common/tapdance/tapdance.go
  21. 1 1
      psiphon/common/tapdance/tapdance_disabled.go
  22. 5 0
      psiphon/config.go
  23. 1 1
      psiphon/controller_test.go
  24. 4 4
      psiphon/dialParameters.go
  25. 1 1
      psiphon/dialParameters_test.go
  26. 1 1
      psiphon/meekConn.go
  27. 8 1
      psiphon/server/tunnelServer.go
  28. 1 0
      psiphon/tunnel.go

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

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

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

@@ -1,6 +1,9 @@
 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'
+    pod 'OCSPCache', :git => "https://github.com/Psiphon-Labs/OCSPCache.git", :commit => 'b945a57'
+    #pod 'OCSPCache', :path => "../../../../../OCSPCache/"
 end
 

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

@@ -0,0 +1,37 @@
+PODS:
+  - OCSPCache (0.1.0):
+    - OpenSSL-Universal (= 1.0.2.17)
+    - ReactiveObjC (= 3.1.1)
+  - OpenSSL-Universal (1.0.2.17)
+  - PsiphonTunnel (2.0.2)
+  - ReactiveObjC (3.1.1)
+
+DEPENDENCIES:
+  - OCSPCache (from `https://github.com/Psiphon-Labs/OCSPCache.git`, commit `b945a57`)
+  - OpenSSL-Universal (= 1.0.2.17)
+  - PsiphonTunnel (from `https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git`)
+
+EXTERNAL SOURCES:
+  OCSPCache:
+    :commit: b945a57
+    :git: https://github.com/Psiphon-Labs/OCSPCache.git
+  PsiphonTunnel:
+    :git: https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git
+
+CHECKOUT OPTIONS:
+  OCSPCache:
+    :commit: b945a57
+    :git: https://github.com/Psiphon-Labs/OCSPCache.git
+  PsiphonTunnel:
+    :commit: c9af3bab93637163e117de9d1e77435baa7646c0
+    :git: https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git
+
+SPEC CHECKSUMS:
+  OCSPCache: 96237607aa9f77ba2fd9119e383b9fb1ef41cfa2
+  OpenSSL-Universal: ff04c2e6befc3f1247ae039e60c93f76345b3b5a
+  PsiphonTunnel: 0c3f8677e4b26316beba57df78ed9c75634ce091
+  ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040
+
+PODFILE CHECKSUM: 9271ea4778a7434c4231ee344501e082af88e1db
+
+COCOAPODS: 1.4.0

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

@@ -51,8 +51,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 +66,7 @@
 		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>"; };
 		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 */
 
@@ -137,6 +138,7 @@
 				662658F91DCB8CF300872F6C /* Info.plist */,
 				6688DBB51DCD684B00721A9E /* psiphon-config.json */,
 				6682D90D1EB1334000329958 /* psiphon-embedded-server-entries.txt */,
+				CEFA7EB82294A9BB0078E41E /* TunneledWebRequest-Bridging-Header.h */,
 			);
 			path = TunneledWebRequest;
 			sourceTree = "<group>";
@@ -240,6 +242,7 @@
 					662658E91DCB8CF300872F6C = {
 						CreatedOnToolsVersion = 8.0;
 						DevelopmentTeam = Q6HLNEX92A;
+						LastSwiftMigration = 1020;
 						ProvisioningStyle = Automatic;
 					};
 					662658FD1DCB8CF400872F6C = {
@@ -261,6 +264,7 @@
 			developmentRegion = English;
 			hasScannedForEncodings = 0;
 			knownRegions = (
+				English,
 				en,
 				Base,
 			);
@@ -474,6 +478,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 +529,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 +540,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 +552,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 +563,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 +575,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 +586,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 +602,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 +617,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 +632,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;

+ 310 - 228
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,48 @@ 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?
+
+    // OCSP cache for making OCSP requests in certificate revocation checking
+    var ocspCache: OCSPCache = OCSPCache.init(logger: {print("[OCSPCache]:", $0)})
+
+    // Delegate for handling certificate validation.
+    lazy var authURLSessionDelegate: OCSPAuthURLSessionDelegate =
+        OCSPAuthURLSessionDelegate.init(logger: {print("[AuthURLSessionTaskDelegate]:", $0)},
+                                        ocspCache: self.ocspCache,
+                                        modifyOCSPURL:{
+                                            assert(self.httpProxyPort > 0)
+
+                                            let encodedTargetURL = URLEncode.encode($0.absoluteString)
+                                            let proxiedURLString = "http://127.0.0.1:\(self.httpProxyPort)/tunneled/\(encodedTargetURL!)"
+                                            let proxiedURL = URL.init(string: proxiedURLString)
+
+                                            print("[OCSP] Updated OCSP URL \($0) to \(proxiedURL!)")
+
+                                            return proxiedURL!
+                                        },
+                                        session:URLSession.shared)
+
+    @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 +79,186 @@ 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?, _ error: 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]()
+        config.timeoutIntervalForRequest = 60 * 5
+
+        // 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: authURLSessionDelegate, 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 {
+                let errorString = "Client-side error in request to \(url): \(String(describing: error))"
+                NSLog(errorString)
+                // Invoke the callback indicating error.
+                completion(nil, errorString)
+                return
+            }
+
+            if data == nil {
+                let errorString = "Data from request to \(url) is nil"
+                NSLog(errorString)
+                // Invoke the callback indicating error.
+                completion(nil, errorString)
+                return
+            }
+
+            let httpResponse = response as? HTTPURLResponse
+            if httpResponse?.statusCode != 200 {
+                let errorString = "Server-side error in request to \(url): \(String(describing: httpResponse))"
+                NSLog(errorString)
+                // Invoke the callback indicating error.
+                completion(nil, errorString)
+                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, nil)
+        }
 
+        // 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?, _ error: 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 {
+                let errorString = "Client-side error in request to \(url): \(String(describing: error))"
+                NSLog(errorString)
+                // Invoke the callback indicating error.
+                completion(nil, errorString)
+                return
+            }
+
+            if data == nil {
+                let errorString = "Data from request to \(url) is nil"
+                NSLog(errorString)
+                // Invoke the callback indicating error.
+                completion(nil, errorString)
+                return
+            }
+
+            let httpResponse = response as? HTTPURLResponse
+            if httpResponse?.statusCode != 200 {
+                let errorString = "Server-side error in request to \(url): \(String(describing: httpResponse))"
+                NSLog(errorString)
+                // Invoke the callback indicating error.
+                completion(nil, errorString)
+                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, nil)
+        }
+
+        // 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 +280,106 @@ extension AppDelegate: TunneledAppDelegate {
         NSLog("onDiagnosticMessage(%@): %@", timestamp, message)
     }
 
-	func onConnected() {
-		NSLog("onConnected")
-
-		// 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
-
-				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!)")
-				}
-
-				// 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!)")
-						}
-
-						// We'll leave the tunnel open for when we want to make
-						// more requests. It will get stopped by `applicationWillTerminate`.
-					}
-				}
-
-			}
-		}
-	}
+    func onConnected() {
+        NSLog("onConnected")
+
+        // 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?, _ error: String?) in
+
+                if let errorString = error?.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("""
+                        Error from \(url):\n\n
+                        \(errorString)\n\n
+                        Using makeRequestViaUrlSessionProxy.\n\n
+                        Check logs for error.
+                        """)
+                    }
+                } else {
+                    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!)")
+                    }
+                }
+
+                // 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?, _ error: String?) in
+
+                        if let errorString = error?.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("""
+                                    Error from \(url):\n\n
+                                    \(errorString)\n\n
+                                    Using makeRequestViaUrlProxy.\n\n
+                                    Check logs for error.
+                                    """)
+                            }
+                            return
+                        } else {
+
+                            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!)")
+                            }
+                        }
+
+                        // 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.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"
   }
-}
-
-
+}

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

@@ -24,10 +24,12 @@
 	<true/>
 	<key>NSAppTransportSecurity</key>
 	<dict>
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
 		<key>NSExceptionDomains</key>
 		<dict>
-			<key>ifconfig.co</key>
-			<string>YES</string>
+			<key>freegeoip.app</key>
+			<string></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 "OCSPAuthURLSessionDelegate.h"
+#import "OCSPURLEncode.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)
     }
 
 }

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

@@ -51,6 +51,7 @@
 #import "JAHPCacheStoragePolicy.h"
 #import "JAHPQNSURLSessionDemux.h"
 
+#import "OCSPAuthURLSessionDelegate.h"
 #import "TunneledWebView-Swift.h"
 
 // I use the following typedef to keep myself sane in the face of the wacky
@@ -171,6 +172,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 +305,7 @@ static NSString * kJAHPRecursiveRequestFlagProperty = @"com.jivesoftware.JAHPAut
             [self authenticatingHTTPProtocol:nil logWithFormat:@"accept request %@", url];
         }
     }
-    
+
     return shouldAccept;
 }
 
@@ -628,11 +630,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 +642,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 +768,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 +780,59 @@ 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.
+        OCSPAuthURLSessionDelegate *authHandler = [[AppDelegate sharedDelegate] authURLSessionDelegate];
+
+        assert(challenge.protectionSpace.serverTrust != nil);
+
+        BOOL evaluateSuccess =
+        [authHandler evaluateTrust:challenge.protectionSpace.serverTrust
+             modifyOCSPURLOverride:nil
+                   sessionOverride:sharedDemuxInstance.session
+                 completionHandler:completionHandler];
+
+        [[self class] authenticatingHTTPProtocol:self
+                                   logWithFormat:@"Evaluate trust for %@ %@",
+                                                 challenge.protectionSpace.host,
+                                                 evaluateSuccess ? @"succeeded": @"failed"];
+
+        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);
     }
 }

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

@@ -1,6 +1,9 @@
 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'
+    pod 'OCSPCache', :git => "https://github.com/Psiphon-Labs/OCSPCache.git", :commit => 'b945a57'
+    #pod 'OCSPCache', :path => "../../../../../OCSPCache/"
 end
 

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

@@ -0,0 +1,37 @@
+PODS:
+  - OCSPCache (0.1.0):
+    - OpenSSL-Universal (= 1.0.2.17)
+    - ReactiveObjC (= 3.1.1)
+  - OpenSSL-Universal (1.0.2.17)
+  - PsiphonTunnel (2.0.2)
+  - ReactiveObjC (3.1.1)
+
+DEPENDENCIES:
+  - OCSPCache (from `https://github.com/Psiphon-Labs/OCSPCache.git`, commit `b945a57`)
+  - OpenSSL-Universal (= 1.0.2.17)
+  - PsiphonTunnel (from `https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git`)
+
+EXTERNAL SOURCES:
+  OCSPCache:
+    :commit: b945a57
+    :git: https://github.com/Psiphon-Labs/OCSPCache.git
+  PsiphonTunnel:
+    :git: https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git
+
+CHECKOUT OPTIONS:
+  OCSPCache:
+    :commit: b945a57
+    :git: https://github.com/Psiphon-Labs/OCSPCache.git
+  PsiphonTunnel:
+    :commit: c9af3bab93637163e117de9d1e77435baa7646c0
+    :git: https://github.com/Psiphon-Labs/psiphon-tunnel-core-iOS-library.git
+
+SPEC CHECKSUMS:
+  OCSPCache: 96237607aa9f77ba2fd9119e383b9fb1ef41cfa2
+  OpenSSL-Universal: ff04c2e6befc3f1247ae039e60c93f76345b3b5a
+  PsiphonTunnel: 0c3f8677e4b26316beba57df78ed9c75634ce091
+  ReactiveObjC: 011caa393aa0383245f2dcf9bf02e86b80b36040
+
+PODFILE CHECKSUM: 4ebe832113ef9b48c7dfb05f087026b45d75b397
+
+COCOAPODS: 1.4.0

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

@@ -295,6 +295,7 @@
 			developmentRegion = English;
 			hasScannedForEncodings = 0;
 			knownRegions = (
+				English,
 				en,
 				Base,
 			);
@@ -586,7 +587,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 +609,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 +623,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 +638,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 +652,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 +666,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;

+ 29 - 6
MobileLibrary/iOS/SampleApps/TunneledWebView/TunneledWebView/AppDelegate.swift

@@ -13,16 +13,35 @@ 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 {
+    // OCSP cache for making OCSP requests in certificate revocation checking
+    var ocspCache: OCSPCache = OCSPCache.init(logger: {print("[OCSPCache]:", $0)})
+
+    // Delegate for handling certificate validation.
+    @objc public lazy var authURLSessionDelegate: OCSPAuthURLSessionDelegate =
+        OCSPAuthURLSessionDelegate.init(logger: {print("[AuthURLSessionTaskDelegate]:", $0)},
+                                        ocspCache: self.ocspCache,
+                                        modifyOCSPURL:{
+                                            assert(self.httpProxyPort > 0)
+
+                                            let encodedTargetURL = URLEncode.encode($0.absoluteString)
+                                            let proxiedURLString = "http://127.0.0.1:\(self.httpProxyPort)/tunneled/\(encodedTargetURL!)"
+                                            let proxiedURL = URL.init(string: proxiedURLString)
+
+                                            print("[OCSP] Updated OCSP URL \($0) to \(proxiedURL!)")
+
+                                            return proxiedURL!},
+                                        session:nil)
+    
+    @objc public class func sharedDelegate() -> AppDelegate {
         var delegate: AppDelegate?
         if (Thread.isMainThread) {
             delegate = UIApplication.shared.delegate as? AppDelegate
@@ -34,7 +53,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 +129,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.
@@ -177,5 +201,4 @@ extension AppDelegate: TunneledAppDelegate {
             self.httpProxyPort = port
         }
     }
-
 }

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

@@ -24,8 +24,12 @@
 	<true/>
 	<key>NSAppTransportSecurity</key>
 	<dict>
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
 		<key>NSExceptionDomains</key>
 		<dict>
+			<key>freegeoip.app</key>
+			<string></string>
 			<key>ifconfig.co</key>
 			<string>YES</string>
 			<key>ip-api.com</key>

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

@@ -10,5 +10,7 @@
 #define TunneledWebView_Bridging_Header_h
 
 #import "JAHPAuthenticatingHTTPProtocol.h"
+#import "OCSPAuthURLSessionDelegate.h"
+#import "OCSPURLEncode.h"
 
 #endif /* TunneledWebView_Bridging_Header_h */

+ 22 - 0
MobileLibrary/iOS/USAGE.md

@@ -48,10 +48,32 @@ 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 each plaintext OCSP request 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 control how revocation checking is performed.
+
+This allows us to perform OCSP requests manually and ensure that they are tunneled. See the comments in [OCSPAuthURLSessionDelegate.h](https://github.com/Psiphon-Labs/OCSPCache/blob/b945a5784cd88ed5693a62a931617bd371f3c9a8/OCSPCache/Classes/OCSPAuthURLSessionDelegate.h) and both sample apps for a reference implementation.
+
+OCSPAuthURLSessionDelegate is part of [OCSPCache](https://github.com/Psiphon-Labs/OCSPCache), which is a CocoaPod developed for constructing OCSP requests and caching OCSP responses.
+
+
 ## 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.

+ 18 - 17
psiphon/common/protocol/protocol.go

@@ -30,17 +30,17 @@ import (
 )
 
 const (
-	TUNNEL_PROTOCOL_SSH                           = "SSH"
-	TUNNEL_PROTOCOL_OBFUSCATED_SSH                = "OSSH"
-	TUNNEL_PROTOCOL_UNFRONTED_MEEK                = "UNFRONTED-MEEK-OSSH"
-	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS          = "UNFRONTED-MEEK-HTTPS-OSSH"
-	TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET = "UNFRONTED-MEEK-SESSION-TICKET-OSSH"
-	TUNNEL_PROTOCOL_FRONTED_MEEK                  = "FRONTED-MEEK-OSSH"
-	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP             = "FRONTED-MEEK-HTTP-OSSH"
-	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH           = "QUIC-OSSH"
-	TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH   = "FRONTED-QUIC-OSSH"
-	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH     = "MARIONETTE-OSSH"
-	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH       = "TAPDANCE-OSSH"
+	TUNNEL_PROTOCOL_SSH                              = "SSH"
+	TUNNEL_PROTOCOL_OBFUSCATED_SSH                   = "OSSH"
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK                   = "UNFRONTED-MEEK-OSSH"
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK_HTTPS             = "UNFRONTED-MEEK-HTTPS-OSSH"
+	TUNNEL_PROTOCOL_UNFRONTED_MEEK_SESSION_TICKET    = "UNFRONTED-MEEK-SESSION-TICKET-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_MEEK                     = "FRONTED-MEEK-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP                = "FRONTED-MEEK-HTTP-OSSH"
+	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH              = "QUIC-OSSH"
+	TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH = "FRONTED-MEEK-QUIC-OSSH"
+	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH        = "MARIONETTE-OSSH"
+	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH          = "TAPDANCE-OSSH"
 
 	SERVER_ENTRY_SOURCE_EMBEDDED   = "EMBEDDED"
 	SERVER_ENTRY_SOURCE_REMOTE     = "REMOTE"
@@ -103,7 +103,7 @@ var SupportedTunnelProtocols = TunnelProtocols{
 	TUNNEL_PROTOCOL_FRONTED_MEEK,
 	TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP,
 	TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH,
-	TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH,
 }
@@ -111,6 +111,7 @@ var SupportedTunnelProtocols = TunnelProtocols{
 var DefaultDisabledTunnelProtocols = TunnelProtocols{
 	TUNNEL_PROTOCOL_MARIONETTE_OBFUSCATED_SSH,
 	TUNNEL_PROTOCOL_TAPDANCE_OBFUSCATED_SSH,
+	TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH,
 }
 
 var SupportedServerEntrySources = TunnelProtocols{
@@ -133,13 +134,13 @@ func TunnelProtocolUsesObfuscatedSSH(protocol string) bool {
 func TunnelProtocolUsesMeek(protocol string) bool {
 	return TunnelProtocolUsesMeekHTTP(protocol) ||
 		TunnelProtocolUsesMeekHTTPS(protocol) ||
-		TunnelProtocolUsesFrontedQUIC(protocol)
+		TunnelProtocolUsesFrontedMeekQUIC(protocol)
 }
 
 func TunnelProtocolUsesFrontedMeek(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_FRONTED_MEEK ||
 		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_HTTP ||
-		protocol == TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH
+		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH
 }
 
 func TunnelProtocolUsesMeekHTTP(protocol string) bool {
@@ -159,11 +160,11 @@ func TunnelProtocolUsesObfuscatedSessionTickets(protocol string) bool {
 
 func TunnelProtocolUsesQUIC(protocol string) bool {
 	return protocol == TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH ||
-		protocol == TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH
+		protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH
 }
 
-func TunnelProtocolUsesFrontedQUIC(protocol string) bool {
-	return protocol == TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH
+func TunnelProtocolUsesFrontedMeekQUIC(protocol string) bool {
+	return protocol == TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH
 }
 
 func TunnelProtocolUsesMarionette(protocol string) bool {

+ 2 - 2
psiphon/common/protocol/serverEntry.go

@@ -184,11 +184,11 @@ func (fields ServerEntryFields) GetConfigurationVersion() int {
 	if !ok {
 		return 0
 	}
-	configurationVersionInt, ok := configurationVersion.(int)
+	configurationVersionFloat, ok := configurationVersion.(float64)
 	if !ok {
 		return 0
 	}
-	return configurationVersionInt
+	return int(configurationVersionFloat)
 }
 
 func (fields ServerEntryFields) GetLocalSource() string {

+ 14 - 9
psiphon/common/tapdance/tapdance.go

@@ -48,11 +48,6 @@ const (
 	READ_PROXY_PROTOCOL_HEADER_TIMEOUT = 5 * time.Second
 )
 
-func init() {
-	refraction_networking_tapdance.Logger().Out = ioutil.Discard
-	refraction_networking_tapdance.EnableProxyProtocol()
-}
-
 // Enabled indicates if Tapdance functionality is enabled.
 func Enabled() bool {
 	return true
@@ -233,13 +228,17 @@ func (conn *tapdanceConn) IsClosed() bool {
 // The Tapdance station config assets are read from dataDirectory/"tapdance".
 // When no config is found, default assets are paved. ctx is expected to have
 // a timeout for the dial.
+//
+// Limitation: the parameters emitLogs and dataDirectory are used for one-time
+// initialization and are ignored after the first Dial call.
 func Dial(
 	ctx context.Context,
+	emitLogs bool,
 	dataDirectory string,
 	netDialer common.NetDialer,
 	address string) (net.Conn, error) {
 
-	err := initAssets(dataDirectory)
+	err := initTapdance(emitLogs, dataDirectory)
 	if err != nil {
 		return nil, common.ContextError(err)
 	}
@@ -288,12 +287,18 @@ func Dial(
 	}, nil
 }
 
-var setAssetsOnce sync.Once
+var initTapdanceOnce sync.Once
 
-func initAssets(dataDirectory string) error {
+func initTapdance(emitLogs bool, dataDirectory string) error {
 
 	var initErr error
-	setAssetsOnce.Do(func() {
+	initTapdanceOnce.Do(func() {
+
+		if !emitLogs {
+			refraction_networking_tapdance.Logger().Out = ioutil.Discard
+		}
+
+		refraction_networking_tapdance.EnableProxyProtocol()
 
 		assetsDir := filepath.Join(dataDirectory, "tapdance")
 

+ 1 - 1
psiphon/common/tapdance/tapdance_disabled.go

@@ -47,6 +47,6 @@ func Listen(_ string) (*Listener, error) {
 }
 
 // Dial establishes a new Tapdance session to a Tapdance station.
-func Dial(_ context.Context, _ string, _ common.NetDialer, _ string) (net.Conn, error) {
+func Dial(_ context.Context, _ bool, _ string, _ common.NetDialer, _ string) (net.Conn, error) {
 	return nil, common.ContextError(disabledError)
 }

+ 5 - 0
psiphon/config.go

@@ -493,6 +493,11 @@ type Config struct {
 	// Required for the exchange functionality.
 	ExchangeObfuscationKey string
 
+	// EmitTapdanceLogs indicates whether to emit gotapdance log messages
+	// to stdout. Note that gotapdance log messages do not conform to the
+	// Notice format standard. Default is off.
+	EmitTapdanceLogs bool
+
 	// TransformHostNameProbability is for testing purposes.
 	TransformHostNameProbability *float64
 

+ 1 - 1
psiphon/controller_test.go

@@ -443,7 +443,7 @@ func TestFrontedQUIC(t *testing.T) {
 	controllerRun(t,
 		&controllerRunConfig{
 			expectNoServerEntries:    false,
-			protocol:                 protocol.TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH,
+			protocol:                 protocol.TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH,
 			clientIsLatestVersion:    false,
 			disableUntunneledUpgrade: true,
 			disableEstablishing:      false,

+ 4 - 4
psiphon/dialParameters.go

@@ -364,7 +364,7 @@ func MakeDialParameters(
 	if !isReplay || !replayHostname {
 
 		if protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol) ||
-			protocol.TunnelProtocolUsesFrontedQUIC(dialParams.TunnelProtocol) {
+			protocol.TunnelProtocolUsesFrontedMeekQUIC(dialParams.TunnelProtocol) {
 
 			dialParams.MeekSNIServerName = ""
 			if p.WeightedCoinFlip(parameters.TransformHostNameProbability) {
@@ -395,7 +395,7 @@ func MakeDialParameters(
 	if (!isReplay || !replayQUICVersion) &&
 		protocol.TunnelProtocolUsesQUIC(dialParams.TunnelProtocol) {
 
-		allowObfuscatedQUIC := !protocol.TunnelProtocolUsesFrontedQUIC(dialParams.TunnelProtocol)
+		allowObfuscatedQUIC := !protocol.TunnelProtocolUsesFrontedMeekQUIC(dialParams.TunnelProtocol)
 		dialParams.QUICVersion = selectQUICVersion(allowObfuscatedQUIC, p)
 	}
 
@@ -441,7 +441,7 @@ func MakeDialParameters(
 	case protocol.TUNNEL_PROTOCOL_QUIC_OBFUSCATED_SSH:
 		dialParams.DirectDialAddress = fmt.Sprintf("%s:%d", serverEntry.IpAddress, serverEntry.SshObfuscatedQUICPort)
 
-	case protocol.TUNNEL_PROTOCOL_FRONTED_QUIC_OBFUSCATED_SSH:
+	case protocol.TUNNEL_PROTOCOL_FRONTED_MEEK_QUIC_OBFUSCATED_SSH:
 		dialParams.MeekDialAddress = fmt.Sprintf("%s:443", dialParams.MeekFrontingDialAddress)
 		dialParams.MeekHostHeader = dialParams.MeekFrontingHost
 		if !dialParams.MeekTransformedHostName {
@@ -576,7 +576,7 @@ func MakeDialParameters(
 		dialParams.meekConfig = &MeekConfig{
 			ClientParameters:              config.clientParameters,
 			DialAddress:                   dialParams.MeekDialAddress,
-			UseQUIC:                       protocol.TunnelProtocolUsesFrontedQUIC(dialParams.TunnelProtocol),
+			UseQUIC:                       protocol.TunnelProtocolUsesFrontedMeekQUIC(dialParams.TunnelProtocol),
 			QUICVersion:                   dialParams.QUICVersion,
 			UseHTTPS:                      protocol.TunnelProtocolUsesMeekHTTPS(dialParams.TunnelProtocol),
 			TLSProfile:                    dialParams.TLSProfile,

+ 1 - 1
psiphon/dialParameters_test.go

@@ -183,7 +183,7 @@ func runDialParametersAndReplay(t *testing.T, tunnelProtocol string) {
 		if dialParams.QUICVersion == "" {
 			t.Fatalf("missing QUIC version field")
 		}
-		if protocol.TunnelProtocolUsesFrontedQUIC(tunnelProtocol) {
+		if protocol.TunnelProtocolUsesFrontedMeekQUIC(tunnelProtocol) {
 			if dialParams.MeekFrontingDialAddress == "" ||
 				dialParams.MeekFrontingHost == "" ||
 				dialParams.MeekSNIServerName == "" {

+ 1 - 1
psiphon/meekConn.go

@@ -594,7 +594,7 @@ func (meek *MeekConn) Close() (err error) {
 		// stopRunning interrupts HTTP requests in progress by closing the context
 		// associated with the request. In the case of h2quic.RoundTripper, testing
 		// indicates that quic-go.receiveStream.readImpl in _not_ interrupted in
-		// this case, and so an in-flight FRONTED-QUIC round trip may hang shutdown
+		// this case, and so an in-flight FRONTED-MEEK-QUIC round trip may hang shutdown
 		// in relayRoundTrip->readPayload->...->quic-go.receiveStream.readImpl.
 		//
 		// To workaround this, we call CloseIdleConnections _before_ Wait, as, in

+ 8 - 1
psiphon/server/tunnelServer.go

@@ -147,7 +147,14 @@ func (server *TunnelServer) Run() error {
 		var listener net.Listener
 		var err error
 
-		if protocol.TunnelProtocolUsesQUIC(tunnelProtocol) {
+		if protocol.TunnelProtocolUsesFrontedMeekQUIC(tunnelProtocol) {
+
+			// For FRONTED-MEEK-QUIC-OSSH, no listener implemented. The edge-to-server
+			// hop uses HTTPS and the client tunnel protocol is distinguished using
+			// protocol.MeekCookieData.ClientTunnelProtocol.
+			continue
+
+		} else if protocol.TunnelProtocolUsesQUIC(tunnelProtocol) {
 
 			listener, err = quic.Listen(
 				CommonLogger(log),

+ 1 - 0
psiphon/tunnel.go

@@ -630,6 +630,7 @@ func dialTunnel(
 
 		dialConn, err = tapdance.Dial(
 			ctx,
+			config.EmitTapdanceLogs,
 			config.DataStoreDirectory,
 			NewNetDialer(dialParams.GetDialConfig()),
 			dialParams.DirectDialAddress)