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 holdingURLSessionTo manage whether the Session needs to be regenerated or shared
  • ProxySessionDelegate:URLSessionOne-to-one correspondence. willURLSessioDelegate 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