0x00 背景
iOS 设置代理的方式常用的有两种:
- 系统设置-->WiFi-->配置代理(HTTP代理)
- 使用科学上网工具全局设置代理 VPN
由于并没有研读 iOS 系统网络库全部功能, 只是使用过AFNetworking 和 Alamofire 的基础功能并没有发现可以设置代理, 但是使用过 curl 请求, 知道是可以直接在请求的时候设置代理 --proxy http://x.x.x.x:7890
所以询问了强大的 ChatGPT, 才知道 iOS 是有一个 URLSessionConfiguration
中的 connectionProxyDictionary
属性, 该属性就是配置代理的字典
0x01 ChatGPT 描述
connectionProxyDictionary
是一个用于配置 NSURLConnection
或 NSURLSession
的代理字典。当你需要使用代理服务器连接到互联网时,你可以使用 connectionProxyDictionary
来指定代理服务器的配置选项。
该字典包含以下键值对:
HTTPEnable
:BOOL
类型,表示是否开启 HTTP 代理。默认为 NO
。
HTTPProxy
:NSString
类型,表示 HTTP 代理服务器的地址。
HTTPPort
:NSInteger
类型,表示 HTTP 代理服务器的端口号。
HTTPSEnable
:BOOL
类型,表示是否开启 HTTPS 代理。默认为 NO
。
HTTPSProxy
:NSString
类型,表示 HTTPS 代理服务器的地址。
HTTPSPort
:NSInteger
类型,表示 HTTPS 代理服务器的端口号。
FTPEnable
:BOOL
类型,表示是否开启 FTP 代理。默认为 NO
。
FTPProxy
:NSString
类型,表示 FTP 代理服务器的地址。
FTPPort
:NSInteger
类型,表示 FTP 代理服务器的端口号。
SOCKSEnable
:BOOL
类型,表示是否开启 SOCKS 代理。默认为 NO
。
SOCKSProxy
:NSString
类型,表示 SOCKS 代理服务器的地址。
SOCKSPort
:NSInteger
类型,表示 SOCKS 代理服务器的端口号。
ProxyAutoConfigEnable
:BOOL
类型,表示是否开启代理自动配置(PAC)。默认为 NO
。
ProxyAutoConfigURLString
:NSString
类型,表示 PAC 配置文件的 URL。
在配置完 connectionProxyDictionary
后,你可以将其传递给 NSURLConnection
或 NSURLSession
对象的初始化方法中来使用代理服务器连接到互联网。
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
进行设置,所以像 http
和 https
这种 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 提供抓包能力, 不需要全局设置了