Recently, a project needed to set up an HTTP proxy when opening a url. So the relevant technical scheme research, and summed up.
The way you set up a Proxy in WebView is to intercept and reprocess the request. There is also a global implementation, NetworkExtension, available after iOS 9, but this will come across as a micropyn App, unfriendly and too heavy.
Using URLProtocol
1. Customize URLProtocol
URLProtocol is an abstract class that intercepts network requests. In practice, you need to customize its subclass.
To use it, you need to register the type of the subclass URLProtocol.
static var isRegistered = false
class func start(a){
guard isRegistered == false else {
return
}
URLProtocol.registerClass(self)
isRegistered = true
}
Copy the code
The core is to override several methods
/// this method is used to process requests, such as adding headers, and returning them without processing
override class func canonicalRequest(for request: URLRequest) - >URLRequest {
return request
}
static let customKey = "HttpProxyProtocolKey"
/// Determine whether the request needs to be processed and mark the customKey attribute with a unique identifier to avoid circular processing
override class func canInit(with request: URLRequest) - >Bool {
guard let url = request.url else {
return false
}
guard letscheme = url.scheme? .lowercased()else {
return false
}
guard scheme == "http" || scheme == "https" else {
return false
}
if let _ = self.property(forKey:customKey, in: request) {
return false
}
return true
}
private var dataTask:URLSessionDataTask?
/// The kernel resends requests in startLoading, sets Proxy information to URLSessionConfigration, and generates URLSession to send requests
override func startLoading(a) {
// 1. Mark the request
let newRequest = request as! NSMutableURLRequest
type(of:self).setProperty(true, forKey: type(of: self).customKey, in: newRequest)
// 2. Configure Proxy
let proxy_server = "YourProxyServer" // proxy server
let proxy_port = 1234 // your port
let hostKey = kCFNetworkProxiesHTTPProxy as String
let portKey = kCFNetworkProxiesHTTPPort as String
let proxyDict:[String:Any] = [kCFNetworkProxiesHTTPEnable as String: true, hostKey:proxy_server, portKey: proxy_port]
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = proxyDict
config.protocolClasses = [type(of:self)]
// 3. Generate URLSession with the configuration
let defaultSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
// 4. Initiate a request
dataTask = defaultSession.dataTask(with:newRequest as URLRequest) dataTask! .resume() }/// Cancel task in stopLoading
override func stopLoading(a){ dataTask? .cancel() }Copy the code
At the same time, the upper callers should be oblivious to interceptions. When this network request is intercepted by URLProtocol, it is necessary to ensure that the network related callbacks or blocks implemented by the upper layer can be called. To address this issue, Apple defines NSURLProtocolClient, a protocol method that covers the entire lifecycle of a network request. A callback or block from the upper caller is executed at the correct time when the method in the protocol is called in its entirety at each stage of the intercepted request.
extension HttpProxyProtocol: URLSessionDataDelegate{
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: (URLSession.ResponseDisposition) -> Void) { client? .urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data){ client? .urlProtocol(self, didLoad: data)
}
}
extension HttpProxyProtocol: URLSessionTaskDelegate{
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
iferror ! =nil&& error! ._code ! =NSURLErrorCancelled{ client? .urlProtocol(self, didFailWithError: error!)
} else{ client? .urlProtocolDidFinishLoading(self)}}}Copy the code
It needs special attention that JS, CSS and Image cannot be accessed after redirection when used in UIWebView. The solution is to add the following code to the redirection method:
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
let newRequest = request as! NSMutableURLRequest
type(of: self).removeProperty(forKey: type(of: self).customKey, in: newRequest) client? .urlProtocol(self, wasRedirectedTo: newRequest as URLRequest, redirectResponse: response) dataTask? .cancel()let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) client? .urlProtocol(self, didFailWithError: error)
}
Copy the code
At this point the complete URLProtocol definition is complete. But one of the downsides is that it creates a new URLSession every time you send a request, which is very inefficient. Apple doesn’t recommend this either, and in some cases it can cause problems such as memory leaks due to incomplete requests. Therefore, we need to share a Session and only regenerate new instances if the agent’s Host or Port changes. The author imitates the practice of Alamofire, a network framework on iOS, and simply writes a SessionManager for management.
2. Customize URLSessionManager
There are two main categories
- ProxySessionManager: Responsible for holding
URLSession
To manage whether the Session needs to be regenerated or shared - ProxySessionDelegate:
URLSession
One-to-one correspondence. willURLSessio
Delegate is allocated to the Delegate of the corresponding Task, and the corresponding Delegate of the Task is maintained
ProxySessionManager basically provides the interface externally, hides the details externally, and configates Delegate and Task generation.
class ProxySessionManager: NSObject {
var host: String?
var port: Int?
static let shared = ProxySessionManager(a)private override init() {}
private var currentSession: URLSession?
private var sessionDelegate: ProxySessionDelegate?
func dataTask(with request: URLRequest, delegate: URLSessionDelegate) -> URLSessionDataTask {
// determine whether a new Session needs to be generated
if let currentSession = currentSession, currentSession.isProxyConfig(host, port){
} else {
let config = URLSessionConfiguration.proxyConfig(host, port)
sessionDelegate = ProxySessionDelegate()
currentSession = URLSession(configuration: config, delegate: self.sessionDelegate, delegateQueue: nil)}letdataTask = currentSession! .dataTask(with: request)/// Save the Task's DelegatesessionDelegate? [dataTask] = delegatereturn dataTask
}
}
Copy the code
And connectionProxyDictionary Settings of the Session Key, no HTTPS. See the constants in the CFNetwork definition, found a kCFNetworkProxiesHTTPSEnable, but is marked as unavailable on iOS, only can be used on MacOS, then we can directly take the constant value set, The corresponding values in the related constants are summarized below.
Raw value | CFNetwork/CFProxySupport.h | CFNetwork/CFHTTPStream.h CFNetwork/CFSocketStream.h |
---|---|---|
"HTTPEnable" |
kCFNetworkProxiesHTTPEnable |
N/A |
"HTTPProxy" |
kCFNetworkProxiesHTTPProxy |
kCFStreamPropertyHTTPProxyHost |
"HTTPPort" |
kCFNetworkProxiesHTTPPort |
kCFStreamPropertyHTTPProxyPort |
"HTTPSEnable" |
kCFNetworkProxiesHTTPSEnable |
N/A |
"HTTPSProxy" |
kCFNetworkProxiesHTTPSProxy |
kCFStreamPropertyHTTPSProxyHost |
"HTTPSPort" |
kCFNetworkProxiesHTTPSPort |
kCFStreamPropertyHTTPSProxyPort |
"SOCKSEnable" |
kCFNetworkProxiesSOCKSEnable |
N/A |
"SOCKSProxy" |
kCFNetworkProxiesSOCKSProxy |
kCFStreamPropertySOCKSProxyHost |
"SOCKSPort" |
kCFNetworkProxiesSOCKSPort |
kCFStreamPropertySOCKSProxyPort |
This allows us to extend the two Extension methods.
fileprivate let httpProxyKey = kCFNetworkProxiesHTTPEnable as String
fileprivate let httpHostKey = kCFNetworkProxiesHTTPProxy as String
fileprivate let httpPortKey = kCFNetworkProxiesHTTPPort as String
fileprivate let httpsProxyKey = "HTTPSEnable"
fileprivate let httpsHostKey = "HTTPSProxy"
fileprivate let httpsPortKey = "HTTPSPort"
extension URLSessionConfiguration{
class func proxyConfig(_ host: String? ._ port: Int?). ->URLSessionConfiguration{
let config = URLSessionConfiguration.ephemeral
if let host = host, let port = port {
let proxyDict:[String:Any] = [httpProxyKey: true,
httpHostKey: host,
httpPortKey: port,
httpsProxyKey: true,
httpsHostKey: host,
httpsPortKey: port]
config.connectionProxyDictionary = proxyDict
}
return config
}
}
extension URLSession{
func isProxyConfig(_aHost: String? ._ aPort: Int?) -> Bool{
if self.configuration.connectionProxyDictionary == nil && aHost == nil && aPort == nil {
return true
} else {
guard let proxyDic = self.configuration.connectionProxyDictionary,
let aHost = aHost,
let aPort = aPort,
let host = proxyDic[httpHostKey] as? String.let port = proxyDic[httpPortKey] as? Int else {
return false
}
if aHost == host, aPort == port{
return true
} else {
return false}}}}Copy the code
ProxySessionDelegate allocates the Delegate to each Task and stores the Delegate of the TaskIdentifer, internally using a dictionary of key-value structures. Lock Settings and values to avoid callback errors.
fileprivate class ProxySessionDelegate: NSObject {
private let lock = NSLock(a)var taskDelegates = [Int: URLSessionDelegate] ()/// borrow from Alamofire to extend the subscript method
subscript(task: URLSessionTask) - >URLSessionDelegate? {
get {
lock.lock()
defer {
lock.unlock()
}
return taskDelegates[task.taskIdentifier]
}
set {
lock.lock()
defer {
lock.unlock()
}
taskDelegates[task.taskIdentifier] = newValue
}
}
}
/// Distribute the callback
extension ProxySessionDelegate: URLSessionDataDelegate{
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
if let delegate = self[dataTask] as? URLSessionDataDelegate{ delegate.urlSession! (session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler) }else {
completionHandler(.cancel)
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if let delegate = self[dataTask] as? URLSessionDataDelegate{ delegate.urlSession! (session, dataTask: dataTask, didReceive: data) } } }extension ProxySessionDelegate: URLSessionTaskDelegate{
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
if let delegate = self[task] as? URLSessionTaskDelegate{ delegate.urlSession? (session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler) } }func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let delegate = self[task] as? URLSessionTaskDelegate{ delegate.urlSession! (session, task: task, didCompleteWithError: error) }self[task] = nil}}Copy the code
In this way, you can create as few URlsessions as possible simply by calling ProxySessionManager or making network requests directly using Alamofire. Apple also has a SampleProject for custom URLProtocol, which is also managed using a singleton.
3. Special handling of WKWebView
Unlike UIWebView, the http&HTTPS Scheme in WKWebView does not use URLPrococol by default. To enable WKWebView to support NSURLProtocol, you need to call the Apple private method to enable WKWebView to allow http&HTTPS Scheme.
The private methods that need to be called are as follows:
[WKBrowsingContextController registerSchemeForCustomProtocol:"http"];
[WKBrowsingContextController registerSchemeForCustomProtocol:"https"];
Copy the code
To use it, you need to call it with reflection
Class cls = NSClassFromString(@"WKBrowsingContextController"); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); If ([(id) CLS respondsToSelector:sel]) {// Assign HTTP and HTTPS requests to NSURLProtocol [(id) CLS performSelector:sel withObject:@"http"]; [(id)cls performSelector:sel withObject:@"https"]; }Copy the code
Which need to bypass the audit check mainly is the name of the class WKBrowsingContextController, in addition to the string can be encrypted or break points, due to the iOS 8.4 above, Can use WKWebview browsingContextController take private method to this type of instance.
Class cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
Copy the code
And then you can use it to greatly reduce the risk, which is written on Swift as follows.
let sel = Selector(("registerSchemeForCustomProtocol:"))
let vc = WKWebView().value(forKey: "browsingContextController") as AnyObject
let cls = type(of: vc) as AnyObject
let _ = cls.perform(sel, with: "http")
let _ = cls.perform(sel, with: "https")
Copy the code
Advantages:
- Powerful interception capability
- Uiwebview and WKWebView are supported
- There are no requirements on the system
Disadvantages:
-
Not friendly enough for WKWebView support
-
Auditing is risky
-
IOS 8.0-8.3 requires additional development (private types & method confusion)
-
The Post request Body data is cleared (this can be resolved using the CanonicalRequest class in Apple SampleProjcet)
-
The ATS is not supported
-
Using WKWebURLSchemeHandler
The WKURLSchemeHandler protocol has been added to WKWebView since iOS 11. You can add processing that follows the WKURLSchemeHandler protocol to a custom Scheme. Where you can add your own processing at start and stop times.
Follow the two methods in the protocol
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
let proxy_server = "YourProxyServer" // proxy server
let proxy_port = 1234 // your port
let hostKey = kCFNetworkProxiesHTTPProxy as String
let portKey = kCFNetworkProxiesHTTPPort as String
let proxyDict:[String:Any] = [kCFNetworkProxiesHTTPEnable as String: true, hostKey:proxy_server, portKey: proxy_port]
let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = proxyDict
let defaultSession = URLSession(configuration: config)
dataTask = defaultSession.dataTask(with: urlSchemeTask.request, completionHandler: {[weak urlSchemeTask] (data, response, error) in
/// urlSchemeTask will crash during callbacks. It is possible that Apple did not consider asynchronous operations in the handler
guard let urlSchemeTask = urlSchemeTask else {
return
}
if let error = error {
urlSchemeTask.didFailWithError(error)
} else {
if let response = response {
urlSchemeTask.didReceive(response)
}
if letdata = data { urlSchemeTask.didReceive(data) } urlSchemeTask.didFinish() } }) dataTask? .resume() }Copy the code
Of course, the processing of URLSession can be reused just like the processing of URLProtocol.
WKWebviewConfiguration is then generated and handler is set up using the official API.
let config = WKWebViewConfiguration()
config.setURLSchemeHandler(HttpProxyHandler(), forURLScheme: "http")// Throw an exception
Copy the code
But since Apple’s setURLSchemeHandler can only be set for custom schemes, schemes like HTTP and HTTPS are handled by default and cannot call this API. You need to set it with the KVC value (this method is invalid on iOS 12.2 and does not exist).
New Solution:
The handlesURLScheme of Hook WKWebView can be used to bypass the system limitation check. The specific code is as follows:
@implementation WKWebView (Hook)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method origin = class_getClassMethod(self, @selector(handlesURLScheme:));
Method hook = class_getClassMethod(self, @selector(cdz_handlesURLScheme:));
method_exchangeImplementations(origin, hook);
});
}
+ (BOOL)cdz_handlesURLScheme:(NSString *)urlScheme {
if ([urlScheme isEqualToString:@"http"] || [urlScheme isEqualToString:@"https"]) {
return NO;
}
return [self cdz_handlesURLScheme:urlScheme];
}
@end
Copy the code
In this case, it can be used smoothly.
extension WKWebViewConfiguration{
class func proxyConifg() - >WKWebViewConfiguration{
let config = WKWebViewConfiguration(a)let handler = HttpProxyHandler()
config.setURLSchemeHandler(handler, forURLScheme: "http")
config.setURLSchemeHandler(handler, forURLScheme: "https")
return config
}
}
Copy the code
Then set up WKWebview to use it.
Advantages:
- Apple official method
- No audit risk
Disadvantages:
- Only iOS 11 or later is supported
- Non-custom schemes are not supported. Other problems may occur if you use an informal Scheme
Using NetworkExtension
To use NetworkExtension, developers need to apply for additional permissions (certificates).
Can establish global VPN, affect global traffic, can obtain global Wifi list, packet capture, and other network related functions.
It can be developed using the third-party library NEKit, and most of the pits have been handled and encapsulated.
Advantages:
- powerful
- Use native functions, no audit risk
Disadvantages:
- The permission application process is complicated
- Only iOS 9 or later is supported (iOS 8 supports only Built-in IPSec and IKEv2 VPNS).
- The native interface implementation is complex and the third party library NEKit pit does not know how many
The last
HttpProxyProtocol, HttpProxyHandler, and HttpProxySessionManager can be used directly in Demo.
Refer to the link
-
IOS apps access the network through a proxy server
-
iOS any body knows how to add a proxy to NSURLRequest?
-
How to set Proxy in web-view swift?
-
CustomHTTPProtocol
-
NSURLProtocol Handling of WKWebView
-
WKWebView the pit
-
Let WKWebView support NSURLProtocol
-
NEKit
-
A preliminary NetworkExtension