ViewController.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. //
  2. // ViewController.swift
  3. // TunneledWebView
  4. //
  5. /*
  6. Licensed under Creative Commons Zero (CC0).
  7. https://creativecommons.org/publicdomain/zero/1.0/
  8. */
  9. import UIKit
  10. import PsiphonTunnel
  11. class ViewController: UIViewController {
  12. var webView: UIWebView!
  13. // The instance of PsiphonTunnel we'll use for connecting.
  14. var psiphonTunnel: PsiphonTunnel?
  15. // These are the ports that we can proxy through.
  16. var socksProxyPort = -1
  17. var httpProxyPort = -1
  18. override func loadView() {
  19. // Make our whole view the webview.
  20. webView = UIWebView()
  21. view = webView
  22. self.psiphonTunnel = PsiphonTunnel.newPsiphonTunnel(self)
  23. }
  24. override func viewDidLoad() {
  25. super.viewDidLoad()
  26. // Start up the tunnel and begin connecting.
  27. // This could be started elsewhere or earlier.
  28. NSLog("Starting tunnel")
  29. let embeddedServerEntries = ""
  30. guard let success = self.psiphonTunnel?.start(embeddedServerEntries), success else {
  31. NSLog("psiphonTunnel.start returned false")
  32. return
  33. }
  34. }
  35. override func didReceiveMemoryWarning() {
  36. super.didReceiveMemoryWarning()
  37. // Dispose of any resources that can be recreated.
  38. }
  39. func appendToView(_ text: String) {
  40. let escapedText = text.replacingOccurrences(of: "\n", with: "\\n")
  41. .replacingOccurrences(of: "\r", with: "")
  42. self.webView.stringByEvaluatingJavaScript(from: String.init(format: "document.body.innerHTML+='<br><pre>%@</pre><br>'", arguments: [escapedText]))
  43. }
  44. /// Request URL using URLSession configured to use the current proxy.
  45. /// * parameters:
  46. /// - url: The URL to request.
  47. /// - completion: A callback function that will received the string obtained
  48. /// from the request, or nil if there's an error.
  49. /// * returns: The string obtained from the request, or nil if there's an error.
  50. func makeRequestViaUrlSessionProxy(_ url: String, completion: @escaping (_ result: String?) -> ()) {
  51. assert(self.httpProxyPort > 0)
  52. let request = URLRequest(url: URL(string: url)!)
  53. let config = URLSessionConfiguration.ephemeral
  54. config.requestCachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
  55. config.connectionProxyDictionary = [AnyHashable: Any]()
  56. // Enable and set the SOCKS proxy values.
  57. config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxy as String] = 1
  58. config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyHost as String] = "127.0.0.1"
  59. config.connectionProxyDictionary?[kCFStreamPropertySOCKSProxyPort as String] = self.socksProxyPort
  60. // Alternatively, the HTTP proxy can be used. Below are the settings for that.
  61. // 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
  62. // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = 1
  63. // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPProxy as String] = "127.0.0.1"
  64. // config.connectionProxyDictionary?[kCFNetworkProxiesHTTPPort as String] = self.httpProxyPort
  65. // config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyHost as String] = "127.0.0.1"
  66. // config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyPort as String] = self.httpProxyPort
  67. let session = URLSession.init(configuration: config, delegate: nil, delegateQueue: OperationQueue.current)
  68. // Create the URLSession task that will make the request via the tunnel proxy.
  69. let task = session.dataTask(with: request) {
  70. (data: Data?, response: URLResponse?, error: Error?) in
  71. if error != nil {
  72. NSLog("Client-side error in request to \(url): \(error)")
  73. // Invoke the callback indicating error.
  74. completion(nil)
  75. return
  76. }
  77. if data == nil {
  78. NSLog("Data from request to \(url) is nil")
  79. // Invoke the callback indicating error.
  80. completion(nil)
  81. return
  82. }
  83. let httpResponse = response as? HTTPURLResponse
  84. if httpResponse?.statusCode != 200 {
  85. NSLog("Server-side error in request to \(url): \(httpResponse)")
  86. // Invoke the callback indicating error.
  87. completion(nil)
  88. return
  89. }
  90. let encodingName = response?.textEncodingName != nil ? response?.textEncodingName : "utf-8"
  91. let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString!))
  92. let stringData = String(data: data!, encoding: String.Encoding(rawValue: UInt(encoding)))
  93. // Make sure the session is cleaned up.
  94. session.invalidateAndCancel()
  95. // Invoke the callback with the result.
  96. completion(stringData)
  97. }
  98. // Start the request task.
  99. task.resume()
  100. }
  101. /// Request URL using Psiphon's "URL proxy" mode.
  102. /// For details, see the comment near the top of:
  103. /// https://github.com/Psiphon-Labs/psiphon-tunnel-core/blob/master/psiphon/httpProxy.go
  104. /// * parameters:
  105. /// - url: The URL to request.
  106. /// - completion: A callback function that will received the string obtained
  107. /// from the request, or nil if there's an error.
  108. /// * returns: The string obtained from the request, or nil if there's an error.
  109. func makeRequestViaUrlProxy(_ url: String, completion: @escaping (_ result: String?) -> ()) {
  110. assert(self.httpProxyPort > 0)
  111. // The target URL must be encoded so as to be valid within a query parameter.
  112. // See this SO answer for why we're using this CharacterSet (and not using: https://stackoverflow.com/a/24888789
  113. let queryParamCharsAllowed = CharacterSet.init(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~")
  114. let encodedTargetURL = url.addingPercentEncoding(withAllowedCharacters: queryParamCharsAllowed)
  115. let proxiedURL = "http://127.0.0.1:\(self.httpProxyPort)/tunneled/\(encodedTargetURL!)"
  116. let task = URLSession.shared.dataTask(with: URL(string: proxiedURL)!) {
  117. (data: Data?, response: URLResponse?, error: Error?) in
  118. if error != nil {
  119. NSLog("Client-side error in request to \(url): \(error)")
  120. // Invoke the callback indicating error.
  121. completion(nil)
  122. return
  123. }
  124. if data == nil {
  125. NSLog("Data from request to \(url) is nil")
  126. // Invoke the callback indicating error.
  127. completion(nil)
  128. return
  129. }
  130. let httpResponse = response as? HTTPURLResponse
  131. if httpResponse?.statusCode != 200 {
  132. NSLog("Server-side error in request to \(url): \(httpResponse)")
  133. // Invoke the callback indicating error.
  134. completion(nil)
  135. return
  136. }
  137. let encodingName = response?.textEncodingName != nil ? response?.textEncodingName : "utf-8"
  138. let encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName as CFString!))
  139. let stringData = String(data: data!, encoding: String.Encoding(rawValue: UInt(encoding)))
  140. // Invoke the callback with the result.
  141. completion(stringData)
  142. }
  143. // Start the request task.
  144. task.resume()
  145. }
  146. }
  147. // MARK: TunneledAppDelegate implementation
  148. // See the protocol definition for details about the methods.
  149. // Note that we're excluding all the optional methods that we aren't using,
  150. // however your needs may be different.
  151. extension ViewController: TunneledAppDelegate {
  152. func getPsiphonConfig() -> String? {
  153. // In this example, we're going to retrieve our Psiphon config from a file in the app bundle.
  154. // Alternatively, it could be a string literal in the code, or whatever makes sense.
  155. guard let psiphonConfigUrl = Bundle.main.url(forResource: "psiphon-config", withExtension: "json") else {
  156. NSLog("Error getting Psiphon config resource file URL!")
  157. return nil
  158. }
  159. do {
  160. return try String.init(contentsOf: psiphonConfigUrl)
  161. } catch {
  162. NSLog("Error getting Psiphon config resource file URL!")
  163. return nil
  164. }
  165. }
  166. func onDiagnosticMessage(_ message: String) {
  167. NSLog("onDiagnosticMessage: %@", message)
  168. }
  169. func onConnected() {
  170. NSLog("onConnected")
  171. // After we're connected, make tunneled requests and populate the webview.
  172. DispatchQueue.global(qos: .default).async {
  173. // First we'll make a "what is my IP" request via makeRequestViaUrlSessionProxy().
  174. let url = "https://freegeoip.net/json/"
  175. self.makeRequestViaUrlSessionProxy(url) {
  176. (_ result: String?) in
  177. if result == nil {
  178. NSLog("Failed to get \(url)")
  179. return
  180. }
  181. // Do a little pretty-printing.
  182. let prettyResult = result?.replacingOccurrences(of: ",", with: ",\n ")
  183. .replacingOccurrences(of: "{", with: "{\n ")
  184. .replacingOccurrences(of: "}", with: "\n}")
  185. DispatchQueue.main.sync {
  186. // Load the result into the view.
  187. self.appendToView("Result from \(url):\n\(prettyResult!)")
  188. }
  189. // Then we'll make a different "what is my IP" request via makeRequestViaUrlProxy().
  190. DispatchQueue.global(qos: .default).async {
  191. let url = "http://ipinfo.io/json"
  192. self.makeRequestViaUrlProxy(url) {
  193. (_ result: String?) in
  194. if result == nil {
  195. NSLog("Failed to get \(url)")
  196. return
  197. }
  198. DispatchQueue.main.sync {
  199. // Load the result into the view.
  200. self.appendToView("Result from \(url):\n\(result!)")
  201. }
  202. // We're done with the Psiphon tunnel, so stop it.
  203. // In a real app, we would keep this alive for as long as we need it.
  204. self.psiphonTunnel?.stop()
  205. }
  206. }
  207. }
  208. }
  209. }
  210. func onListeningSocksProxyPort(_ port: Int) {
  211. NSLog("onListeningSocksProxyPort: %d", port)
  212. // Record the port being used so that we can proxy through it later.
  213. DispatchQueue.main.async {
  214. self.socksProxyPort = port
  215. }
  216. }
  217. func onListeningHttpProxyPort(_ port: Int) {
  218. NSLog("onListeningHttpProxyPort: %d", port)
  219. // Record the port being used so that we can proxy through it later.
  220. DispatchQueue.main.async {
  221. self.httpProxyPort = port
  222. }
  223. }
  224. }