美文网首页
为 iOS 网络请求设置代理

为 iOS 网络请求设置代理

作者: 游城十代2dai | 来源:发表于2023-03-12 19:19 被阅读0次

0x00 背景

iOS 设置代理的方式常用的有两种:

  • 系统设置-->WiFi-->配置代理(HTTP代理)
  • 使用科学上网工具全局设置代理 VPN

由于并没有研读 iOS 系统网络库全部功能, 只是使用过AFNetworking 和 Alamofire 的基础功能并没有发现可以设置代理, 但是使用过 curl 请求, 知道是可以直接在请求的时候设置代理 --proxy http://x.x.x.x:7890

所以询问了强大的 ChatGPT, 才知道 iOS 是有一个 URLSessionConfiguration 中的 connectionProxyDictionary 属性, 该属性就是配置代理的字典

0x01 ChatGPT 描述

connectionProxyDictionary 是一个用于配置 NSURLConnectionNSURLSession 的代理字典。当你需要使用代理服务器连接到互联网时,你可以使用 connectionProxyDictionary 来指定代理服务器的配置选项。

该字典包含以下键值对:

HTTPEnableBOOL 类型,表示是否开启 HTTP 代理。默认为 NO
HTTPProxyNSString 类型,表示 HTTP 代理服务器的地址。
HTTPPortNSInteger 类型,表示 HTTP 代理服务器的端口号。
HTTPSEnableBOOL 类型,表示是否开启 HTTPS 代理。默认为 NO
HTTPSProxyNSString 类型,表示 HTTPS 代理服务器的地址。
HTTPSPortNSInteger 类型,表示 HTTPS 代理服务器的端口号。
FTPEnableBOOL 类型,表示是否开启 FTP 代理。默认为 NO
FTPProxyNSString 类型,表示 FTP 代理服务器的地址。
FTPPortNSInteger 类型,表示 FTP 代理服务器的端口号。
SOCKSEnableBOOL 类型,表示是否开启 SOCKS 代理。默认为 NO
SOCKSProxyNSString 类型,表示 SOCKS 代理服务器的地址。
SOCKSPortNSInteger 类型,表示 SOCKS 代理服务器的端口号。
ProxyAutoConfigEnableBOOL 类型,表示是否开启代理自动配置(PAC)。默认为 NO
ProxyAutoConfigURLStringNSString 类型,表示 PAC 配置文件的 URL。

在配置完 connectionProxyDictionary 后,你可以将其传递给 NSURLConnectionNSURLSession 对象的初始化方法中来使用代理服务器连接到互联网。

0x02 尝试

简单撸一个代码需要科学上网的代码:

    func test() {
        var request = URLRequest(url: URL(string: "https://www.google.com/search?q=hello")!, timeoutInterval: 10)
        request.httpMethod = "GET"
        
        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config)

        let task = session.dataTask(with: request) { data, response, error in
          guard let data = data else {
            print(String(describing: error))
            return
          }
          print(String(data: data, encoding: .ascii)!)
        }

        task.resume()
    }

直接进行 run 的话, 达到超时时间会有以下日志输出:

Optional(Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={_kCFStreamErrorCodeKey=-2102, NSUnderlyingError=0x280148cc0 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <09CB8002-4D12-4D91-B722-1FE53425A66B>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
    "LocalDataTask <09CB8002-4D12-4D91-B722-1FE53425A66B>.<1>"
), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=https://www.google.com/search?q=hello, NSErrorFailingURLKey=https://www.google.com/search?q=hello, _kCFStreamErrorDomainKey=4})

根据 ChatGPT 给的提示, 尝试添加 connectionProxyDictionary 属性:

    func test() {
        var request = URLRequest(url: URL(string: "https://www.google.com/search?q=hello")!, timeoutInterval: Double.infinity)
        request.httpMethod = "GET"
        
        let config = URLSessionConfiguration.default
        config.connectionProxyDictionary = [
            "HTTPEnable": true,
            "HTTPProxy": "10.240.9.20",
            "HTTPPort": 7890
        ]
        let session = URLSession(configuration: config)

        let task = session.dataTask(with: request) { data, response, error in
          guard let data = data else {
            print(String(describing: error))
            return
          }
          print(String(data: data, encoding: .ascii)!)
        }

        task.resume()
    }

结果依旧是超时😂😂😂, 随后发现我的 url 是 https 的, 修改后的代码:

    func test() {
        var request = URLRequest(url: URL(string: "https://www.google.com/search?q=hello")!, timeoutInterval: Double.infinity)
        request.httpMethod = "GET"
        
        let config = URLSessionConfiguration.default
        config.connectionProxyDictionary = [
            "HTTPEnable": true,
            "HTTPProxy": "10.240.9.20",
            "HTTPPort": 7890,
            "HTTPSEnable": true,
            "HTTPSProxy": "10.240.9.20",
            "HTTPSPort": 7890
        ]
        let session = URLSession(configuration: config)

        let task = session.dataTask(with: request) { data, response, error in
          guard let data = data else {
            print(String(describing: error))
            return
          }
          print(String(data: data, encoding: .ascii)!)
        }

        task.resume()
    }

此时就通透了, 日志输出的是一段 html

0x03 WKWebView 代理

做普通的 iOS 端内请求已经可以做到代理了, 那 WKWebView 呢?

查询资料后得知: iOS 11 以上,苹果为 WKWebView 增加了 WKURLSchemeHandler 协议,可以为自定义的 Scheme 增加遵循 WKURLSchemeHandler 协议的处理。其中可以在 start 和 stop 的时机增加自己的处理。

由于苹果的 setURLSchemeHandler 只能对自定义的 Scheme 进行设置,所以像 httphttps 这种 Scheme,需要通过 hook 系统方法来绕过系统的限制检查

参考 iOSHttpProxyDemo 内的 HttpProxyHandler, 对其进行简单的修改得到如下代码(代码后有注意事项):

import Foundation
import WebKit
import ObjectiveC

final class HttpProxyHandler: NSObject {
    private var dataTasks: [String: URLSessionDataTask] = [:]
    private let proxyConfig: HttpProxyConfig
    
    init(proxyConfig: HttpProxyConfig) {
        self.proxyConfig = proxyConfig
    }
}

extension HttpProxyHandler: WKURLSchemeHandler {
    func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
        var request = urlSchemeTask.request
        request.addValue(MMNetworkConfig.shared().cookie ?? "", forHTTPHeaderField: "Cookie")
        let config = URLSessionConfiguration.default
        config.addProxyConfig(proxyConfig)
        let session = URLSession(configuration: config)
        let dataTask = session.dataTask(with: request) { [weak urlSchemeTask] data, response, error in
            guard let urlSchemeTask = urlSchemeTask else { return }
            if let error = error, error._code != NSURLErrorCancelled {
                urlSchemeTask.didFailWithError(error)
            } else {
                if let response = response {
                    urlSchemeTask.didReceive(response)
                }
                if let data = data {
                    urlSchemeTask.didReceive(data)
                }
                urlSchemeTask.didFinish()
            }
        }
        dataTask.resume()
        dataTasks[request.url?.absoluteString ?? ""] = dataTask
    }

    func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
        dataTasks[urlSchemeTask.request.url?.absoluteString ?? ""]?.cancel()
    }
}

private var hookWKWebView: () = {
    guard let origin = class_getClassMethod(WKWebView.self, #selector(WKWebView.handlesURLScheme(_:))),
          let hook = class_getClassMethod(WKWebView.self, #selector(WKWebView._handlesURLScheme(_:))) else {
        return
    }
    method_exchangeImplementations(origin, hook)
}()

fileprivate extension WKWebView {
    @objc static func _handlesURLScheme(_ urlScheme: String) -> Bool {
        if httpSchemes.contains(urlScheme) {
            return false
        }
        return Self.handlesURLScheme(urlScheme)
    }
}

extension WKWebViewConfiguration {
    func addProxyConfig(_ config: HttpProxyConfig) {
        let handler = HttpProxyHandler(proxyConfig: config)
        _ = hookWKWebView
        httpSchemes.forEach {
            setURLSchemeHandler(handler, forURLScheme: $0)
        }
    }
}

需要注意的是, Cookie 会在代理的时候丢失所以需要再这个代码中重新设置一下 Cookie信息

0x04 应用在工程

由于参考 iOSHttpProxyDemo , 发现使用 URLProtocol 做全局拦截, 然后统一修改代理, 是个很好的方案, 可是现实情况并不好, 这种拦截只支持 URLLoadingSystem, 也就是 URLSession.shared

对于我们的工程使用的是 AFNetworking, 它的 URLSession 创建使用的是 let session = URLSession(configuration: config), 就需要对所有的 config 赋值 connectionProxyDictionary 属性

本来想要用 runtime 处理, 后面还是觉得直接写个赋值的方法, 至少看的清晰, 如果真的需要对所有的网络请求都做拦截, 还是需要尝试使用 runtime 来解决吧, 没有再进行深入了解了, 已经满足研究背景了

0x05 结语

我的代理是本地的, 通过 clash 做的局域网代理, 上面的请求就是通过代码代理到我电脑本地, 然后通过电脑本地的 clash 工具科学上网的

connectionProxyDictionary 的赋值不要偷懒, http/https 做代理的话记得同时写上, 上面就是因为偷懒导致尝试了几次都是超时🤣

按照 ChatGPT 的描述, 还可以使用 socks 代理, 也可以直接使用 PAC 文件代理, 玩法也挺多的, 这样写的代码只针对自己开发过程中的 app 提供抓包能力, 不需要全局设置了

参考链接:

iOS 设置代理(Proxy)方案总结
iOSHttpProxyDemo

相关文章

网友评论

      本文标题:为 iOS 网络请求设置代理

      本文链接:https://www.haomeiwen.com/subject/ldafrdtx.html