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è)用于配置 NSURLConnection
或 NSURLSession
的代理字典。當(dāng)你需要使用代理服務(wù)器連接到互聯(lián)網(wǎng)時(shí)蚌堵,你可以使用 connectionProxyDictionary
來指定代理服務(wù)器的配置選項(xiàng)买决。
該字典包含以下鍵值對(duì):
HTTPEnable
:BOOL
類型,表示是否開啟 HTTP 代理吼畏。默認(rèn)為 NO
策州。
HTTPProxy
:NSString
類型,表示 HTTP 代理服務(wù)器的地址宫仗。
HTTPPort
:NSInteger
類型,表示 HTTP 代理服務(wù)器的端口號(hào)旁仿。
HTTPSEnable
:BOOL
類型藕夫,表示是否開啟 HTTPS 代理。默認(rèn)為 NO
枯冈。
HTTPSProxy
:NSString
類型毅贮,表示 HTTPS 代理服務(wù)器的地址。
HTTPSPort
:NSInteger
類型尘奏,表示 HTTPS 代理服務(wù)器的端口號(hào)滩褥。
FTPEnable
:BOOL
類型,表示是否開啟 FTP 代理炫加。默認(rèn)為 NO
瑰煎。
FTPProxy
:NSString
類型铺然,表示 FTP 代理服務(wù)器的地址。
FTPPort
:NSInteger
類型酒甸,表示 FTP 代理服務(wù)器的端口號(hào)魄健。
SOCKSEnable
:BOOL
類型,表示是否開啟 SOCKS 代理插勤。默認(rèn)為 NO
沽瘦。
SOCKSProxy
:NSString
類型,表示 SOCKS 代理服務(wù)器的地址农尖。
SOCKSPort
:NSInteger
類型析恋,表示 SOCKS 代理服務(wù)器的端口號(hào)。
ProxyAutoConfigEnable
:BOOL
類型盛卡,表示是否開啟代理自動(dòng)配置(PAC)助隧。默認(rèn)為 NO
。
ProxyAutoConfigURLString
:NSString
類型窟扑,表示 PAC 配置文件的 URL喇颁。
在配置完 connectionProxyDictionary
后,你可以將其傳遞給 NSURLConnection
或 NSURLSession
對(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è)置哆致,所以像 http
和 https
這種 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è)置了