為 iOS 網(wǎng)絡(luò)請(qǐng)求設(shè)置代理

0x00 背景

iOS 設(shè)置代理的方式常用的有兩種:

  • 系統(tǒng)設(shè)置-->WiFi-->配置代理(HTTP代理)
  • 使用科學(xué)上網(wǎng)工具全局設(shè)置代理 VPN

由于并沒有研讀 iOS 系統(tǒng)網(wǎng)絡(luò)庫全部功能, 只是使用過AFNetworking 和 Alamofire 的基礎(chǔ)功能并沒有發(fā)現(xiàn)可以設(shè)置代理, 但是使用過 curl 請(qǐng)求, 知道是可以直接在請(qǐng)求的時(shí)候設(shè)置代理 --proxy http://x.x.x.x:7890

所以詢問了強(qiáng)大的 ChatGPT, 才知道 iOS 是有一個(gè) URLSessionConfiguration 中的 connectionProxyDictionary 屬性, 該屬性就是配置代理的字典

0x01 ChatGPT 描述

connectionProxyDictionary 是一個(gè)用于配置 NSURLConnectionNSURLSession 的代理字典。當(dāng)你需要使用代理服務(wù)器連接到互聯(lián)網(wǎng)時(shí)蚌堵,你可以使用 connectionProxyDictionary 來指定代理服務(wù)器的配置選項(xiàng)买决。

該字典包含以下鍵值對(duì):

HTTPEnableBOOL 類型,表示是否開啟 HTTP 代理吼畏。默認(rèn)為 NO策州。
HTTPProxyNSString 類型,表示 HTTP 代理服務(wù)器的地址宫仗。
HTTPPortNSInteger 類型,表示 HTTP 代理服務(wù)器的端口號(hào)旁仿。
HTTPSEnableBOOL 類型藕夫,表示是否開啟 HTTPS 代理。默認(rèn)為 NO枯冈。
HTTPSProxyNSString 類型毅贮,表示 HTTPS 代理服務(wù)器的地址。
HTTPSPortNSInteger 類型尘奏,表示 HTTPS 代理服務(wù)器的端口號(hào)滩褥。
FTPEnableBOOL 類型,表示是否開啟 FTP 代理炫加。默認(rèn)為 NO瑰煎。
FTPProxyNSString 類型铺然,表示 FTP 代理服務(wù)器的地址。
FTPPortNSInteger 類型酒甸,表示 FTP 代理服務(wù)器的端口號(hào)魄健。
SOCKSEnableBOOL 類型,表示是否開啟 SOCKS 代理插勤。默認(rèn)為 NO沽瘦。
SOCKSProxyNSString 類型,表示 SOCKS 代理服務(wù)器的地址农尖。
SOCKSPortNSInteger 類型析恋,表示 SOCKS 代理服務(wù)器的端口號(hào)。
ProxyAutoConfigEnableBOOL 類型盛卡,表示是否開啟代理自動(dòng)配置(PAC)助隧。默認(rèn)為 NO
ProxyAutoConfigURLStringNSString 類型窟扑,表示 PAC 配置文件的 URL喇颁。

在配置完 connectionProxyDictionary 后,你可以將其傳遞給 NSURLConnectionNSURLSession 對(duì)象的初始化方法中來使用代理服務(wù)器連接到互聯(lián)網(wǎng)嚎货。

0x02 嘗試

簡單擼一個(gè)代碼需要科學(xué)上網(wǎng)的代碼:

    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()
    }

直接進(jìn)行 run 的話, 達(dá)到超時(shí)時(shí)間會(huì)有以下日志輸出:

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})

根據(jù) 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()
    }

結(jié)果依舊是超時(shí)??????, 隨后發(fā)現(xiàn)我的 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()
    }

此時(shí)就通透了, 日志輸出的是一段 html

0x03 WKWebView 代理

做普通的 iOS 端內(nèi)請(qǐng)求已經(jīng)可以做到代理了, 那 WKWebView 呢?

查詢資料后得知: iOS 11 以上橘霎,蘋果為 WKWebView 增加了 WKURLSchemeHandler 協(xié)議殖属,可以為自定義的 Scheme 增加遵循 WKURLSchemeHandler 協(xié)議的處理外潜。其中可以在 start 和 stop 的時(shí)機(jī)增加自己的處理玄组。

由于蘋果的 setURLSchemeHandler 只能對(duì)自定義的 Scheme 進(jìn)行設(shè)置哆致,所以像 httphttps 這種 Scheme摊阀,需要通過 hook 系統(tǒng)方法來繞過系統(tǒng)的限制檢查

參考 iOSHttpProxyDemo 內(nèi)的 HttpProxyHandler, 對(duì)其進(jìn)行簡單的修改得到如下代碼(代碼后有注意事項(xiàng)):

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 會(huì)在代理的時(shí)候丟失所以需要再這個(gè)代碼中重新設(shè)置一下 Cookie信息

0x04 應(yīng)用在工程

由于參考 iOSHttpProxyDemo , 發(fā)現(xiàn)使用 URLProtocol 做全局?jǐn)r截, 然后統(tǒng)一修改代理, 是個(gè)很好的方案, 可是現(xiàn)實(shí)情況并不好, 這種攔截只支持 URLLoadingSystem, 也就是 URLSession.shared

對(duì)于我們的工程使用的是 AFNetworking, 它的 URLSession 創(chuàng)建使用的是 let session = URLSession(configuration: config), 就需要對(duì)所有的 config 賦值 connectionProxyDictionary 屬性

本來想要用 runtime 處理, 后面還是覺得直接寫個(gè)賦值的方法, 至少看的清晰, 如果真的需要對(duì)所有的網(wǎng)絡(luò)請(qǐng)求都做攔截, 還是需要嘗試使用 runtime 來解決吧, 沒有再進(jìn)行深入了解了, 已經(jīng)滿足研究背景了

0x05 結(jié)語

我的代理是本地的, 通過 clash 做的局域網(wǎng)代理, 上面的請(qǐng)求就是通過代碼代理到我電腦本地, 然后通過電腦本地的 clash 工具科學(xué)上網(wǎng)的

connectionProxyDictionary 的賦值不要偷懶, http/https 做代理的話記得同時(shí)寫上, 上面就是因?yàn)橥祽袑?dǎo)致嘗試了幾次都是超時(shí)??

按照 ChatGPT 的描述, 還可以使用 socks 代理, 也可以直接使用 PAC 文件代理, 玩法也挺多的, 這樣寫的代碼只針對(duì)自己開發(fā)過程中的 app 提供抓包能力, 不需要全局設(shè)置了

參考鏈接:

iOS 設(shè)置代理(Proxy)方案總結(jié)
iOSHttpProxyDemo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灵临,一起剝皮案震驚了整個(gè)濱河市宦焦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌精堕,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毙籽,死亡現(xiàn)場離奇詭異巡扇,居然都是意外死亡乖坠,警方通過查閱死者的電腦和手機(jī)仰迁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門雌隅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肯污,你說我怎么就攤上這事貌亭∪ǘ海” “怎么了恕酸?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵义矛,是天一觀的道長了讨。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么前计? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任胞谭,我火速辦了婚禮,結(jié)果婚禮上男杈,老公的妹妹穿的比我還像新娘丈屹。我一直安慰自己,他們只是感情好伶棒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布旺垒。 她就那樣靜靜地躺著,像睡著了一般苞冯。 火紅的嫁衣襯著肌膚如雪袖牙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天舅锄,我揣著相機(jī)與錄音鞭达,去河邊找鬼。 笑死皇忿,一個(gè)胖子當(dāng)著我的面吹牛畴蹭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鳍烁,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼叨襟,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了幔荒?” 一聲冷哼從身側(cè)響起糊闽,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎爹梁,沒想到半個(gè)月后右犹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡姚垃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年念链,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片积糯。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡掂墓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出看成,到底是詐尸還是另有隱情君编,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布川慌,位于F島的核電站啦粹,受9級(jí)特大地震影響偿荷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唠椭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望忍饰。 院中可真熱鬧贪嫂,春花似錦、人聲如沸艾蓝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赢织。三九已至亮靴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間于置,已是汗流浹背茧吊。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留八毯,地道東北人搓侄。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像话速,于是被迫代替她去往敵國和親讶踪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容