Răsfoiți Sursa

Add URL proxy example. Refactor.

Adam Pritchard 9 ani în urmă
părinte
comite
8257aef6ce

+ 173 - 58
MobileLibrary/iOS/SampleApps/TunneledWebRequest/TunneledWebRequest/ViewController.swift

@@ -19,7 +19,7 @@ class ViewController: UIViewController {
     // The instance of PsiphonTunnel we'll use for connecting.
     var psiphonTunnel: PsiphonTunnel?
     
-    // This are the ports that we can proxy through.
+    // These are the ports that we can proxy through.
     var socksProxyPort = -1
     var httpProxyPort = -1
     
@@ -33,12 +33,10 @@ class ViewController: UIViewController {
     
     override func viewDidLoad() {
         super.viewDidLoad()
-        // Do any additional setup after loading the view, typically from a nib.
         
         // Start up the tunnel and begin connecting.
         // This could be started elsewhere or earlier.
         NSLog("Starting tunnel")
-        
         let embeddedServerEntries = ""
         guard let success = self.psiphonTunnel?.start(embeddedServerEntries), success else {
             NSLog("psiphonTunnel.start returned false")
@@ -50,11 +48,144 @@ class ViewController: UIViewController {
         super.didReceiveMemoryWarning()
         // Dispose of any resources that can be recreated.
     }
+    
+    func appendToView(_ text: String) {
+        let escapedText = text.replacingOccurrences(of: "\n", with: "\\n")
+                              .replacingOccurrences(of: "\r", with: "")
+        self.webView.stringByEvaluatingJavaScript(from: String.init(format: "document.body.innerHTML+='<br><pre>%@</pre><br>'", arguments: [escapedText]))
+    }
+    
+    /// 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?) -> ()) {
+        assert(self.httpProxyPort > 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] = self.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): \(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): \(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?) -> ()) {
+        assert(self.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:\(self.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): \(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): \(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, 
+// Note that we're excluding all the optional methods that we aren't using,
 // however your needs may be different.
 extension ViewController: TunneledAppDelegate {
     func getPsiphonConfig() -> String? {
@@ -81,68 +212,52 @@ extension ViewController: TunneledAppDelegate {
     func onConnected() {
         NSLog("onConnected")
         
-        // After we're connected, make a tunneled request and populate the webview.
+        // After we're connected, make tunneled requests and populate the webview.
         
-        DispatchQueue.main.async {
-            assert(self.httpProxyPort > 0)
-            
-            // We'll check out IP to make sure we're tunneled.
-            let urlPath: String = "https://freegeoip.net/csv/"
-            let url = URL(string: urlPath)!
-            let request = URLRequest(url: 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] = self.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 \(urlPath): \(error)")
-                    return
-                }
+        DispatchQueue.global(qos: .default).async {
+            // First we'll make a "what is my IP" request via makeRequestViaUrlSessionProxy().
+            let url = "https://freegeoip.net/json/"
+            self.makeRequestViaUrlSessionProxy(url) {
+                (_ result: String?) in
                 
-                let httpResponse = response as? HTTPURLResponse
-                if httpResponse?.statusCode != 200 {
-                    NSLog("Server-side error in request to \(urlPath): \(httpResponse)")
+                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}")
                 
-                let encodingName = response?.textEncodingName != nil ? response?.textEncodingName : "utf-8"
-                let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString!))
-                
-                var stringData = String(data: data!, encoding: String.Encoding(rawValue: UInt(encoding)))
-                stringData = stringData?.replacingOccurrences(of: ",", with: "\n")
-                
-                // Load the IP info result into the web view.
-                self.webView.loadHTMLString("<br><pre>\(stringData!)</pre>", baseURL: url)
+                DispatchQueue.main.sync {
+                    // Load the result into the view.
+                    self.appendToView("Result from \(url):\n\(prettyResult!)")
+                }
                 
-                // Make sure the session is cleaned up.
-                session.invalidateAndCancel()
+                // Then we'll make a different "what is my IP" request via makeRequestViaUrlProxy().
+                DispatchQueue.global(qos: .default).async {
+                    let url = "http://ipinfo.io/json"
+                    self.makeRequestViaUrlProxy(url) {
+                        (_ result: String?) in
+                        
+                        if result == nil {
+                            NSLog("Failed to get \(url)")
+                            return
+                        }
+                        
+                        DispatchQueue.main.sync {
+                            // Load the result into the view.
+                            self.appendToView("Result from \(url):\n\(result!)")
+                        }
+                        
+                        // We're done with the Psiphon tunnel, so stop it.
+                        // In a real app, we would keep this alive for as long as we need it.
+                        self.psiphonTunnel?.stop()
+                    }
+                }
                 
-                // We're done with the Psiphon tunnel, so stop it.
-                // In a real app, we would keep this alive for as long as we need it.
-                self.psiphonTunnel?.stop()
             }
-            
-            // Start the request task.
-            task.resume()
         }
     }