Sources/Instrumentation/WKWebView/URLProtocol/WKWebViewBodyCacheURLProtocol.swift (164 lines of code) (raw):
//
// Copyright 2023 aliyun-sls Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import Foundation
import WebKit
// MARK: - URLProtocol for cache XHR request body
// MARK: - register & unregister
class WKWebViewBodyCacheURLProtocol : URLProtocol {
static var WKWebViewProtocolKey = "kOTelJSBridgeNSURLProtocolKey"
static var WKWebViewBridgeRequestId = "OTelJSBridge-RequestId"
var requestId: String?
var requestMethod: String?
var customTask : URLSessionDataTask?
static func register() {
self.registerScheme(scheme: "http")
self.registerScheme(scheme: "https")
URLProtocol.registerClass(WKWebViewBodyCacheURLProtocol.self)
}
static func unregister() {
self.unregisterScheme(scheme: "http")
self.unregisterScheme(scheme: "https")
URLProtocol.unregisterClass(WKWebViewBodyCacheURLProtocol.self)
}
}
// MARK: - URLProtocol implementation
extension WKWebViewBodyCacheURLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
if let bool: Bool = URLProtocol.property(forKey: WKWebViewProtocolKey, in: request) as? Bool, bool {
return false
}
if let host = request.url?.absoluteString, host.contains(WKWebViewBridgeRequestId) {
return true
}
return false
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool {
return super.requestIsCacheEquivalent(a, to: b)
}
}
// MARK: - loading url
extension WKWebViewBodyCacheURLProtocol {
override func startLoading() {
guard let mutableRequest = (self.request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {
return
}
// set a flag to prevent loop
URLProtocol.setProperty(true, forKey: WKWebViewBodyCacheURLProtocol.WKWebViewProtocolKey, in: mutableRequest)
var requestId: String?
if let absoluteUrl = mutableRequest.url?.absoluteString, absoluteUrl.contains(WKWebViewBodyCacheURLProtocol.WKWebViewBridgeRequestId) {
requestId = self.fetchRequestId(url: absoluteUrl)
if let requestIdPair = self.fetchRequestIdPair(url: absoluteUrl) {
// remove requestId pair before send url request
mutableRequest.url = URL.init(string: absoluteUrl.replacingOccurrences(of: requestIdPair, with: ""))
}
}
self.requestId = requestId
self.requestMethod = mutableRequest.httpMethod
// sync cookie
var request = mutableRequest as URLRequest
WebViewCookieManager.syncRequestCookie(request: &request)
// set http body
if let requestId = requestId, let httpMethod = request.httpMethod, httpMethod.count > 0 {
let methods = ["GET"]
if !methods.contains(httpMethod) {
if let bodyRequest = XMLBodyCacheRequest.getRequestBody(requestId: requestId) {
AjaxBodyHelper.setBodyRequest(bodyRequest: bodyRequest, request: &request)
}
}
}
// TODO: 外部代理
// print("WKWebViewBodyCacheURLProtocol. method: \(request.httpMethod!), url: \(request.url!), path: \(request.url!.path), headers: \(request.allHTTPHeaderFields!)")
let session = URLSession.init(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
self.customTask = session.dataTask(with: request)
self.customTask?.resume()
}
override func stopLoading() {
if let _ = customTask {
customTask!.cancel()
customTask = nil
}
self.clearRequestBody()
}
func clearRequestBody() {
/**
参考
全部的 method
http://www.iana.org/assignments/http-methods/http-methods.xhtml
https://stackoverflow.com/questions/41411152/how-many-http-verbs-are-there
Http 1.1
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods
HTTP Extensions WebDAV
http://www.webdav.org/specs/rfc4918.html#http.methods.for.distributed.authoring
*/
// 清除缓存
// 针对有 body 的 method,才需要清除 body 缓存
let methods = ["POST", "PUT", "DELETE", "PATCH", "LOCK", "PROPFIND", "PROPPATCH", "SEARCH"]
if let requestMethod = requestMethod, requestMethod.count > 0, methods.contains(requestMethod) {
XMLBodyCacheRequest.deleteRequestBody(requestId: self.requestId)
}
}
}
extension WKWebViewBodyCacheURLProtocol {
static let URL_REQUEST_ID_REGEX = "^.*?[&|\\?|%3f]?OTelJSBridge-RequestId[=|%3d](\\d+).*?$"
static let URL_REQUEST_ID_PAIR_REGEX = "^.*?([&|\\?|%3f]?OTelJSBridge-RequestId[=|%3d]\\d+).*?$"
func fetchRequestId(url: String) -> String? {
return self.getMatchedTextFromUrl(url: url, regex: WKWebViewBodyCacheURLProtocol.URL_REQUEST_ID_REGEX)
}
func fetchRequestIdPair(url: String) -> String? {
return self.getMatchedTextFromUrl(url: url, regex: WKWebViewBodyCacheURLProtocol.URL_REQUEST_ID_PAIR_REGEX)
}
func getMatchedTextFromUrl(url: String, regex: String) -> String? {
do {
let regexExpression = try NSRegularExpression(pattern: regex, options: NSRegularExpression.Options.caseInsensitive)
var content: String?
let matches = regexExpression.matches(in: url, range: NSMakeRange(0, url.count))
for match in matches {
for i in 0...match.numberOfRanges {
content = (url as NSString).substring(with: match.range(at: i))
if (1 == i) {
return content
}
}
}
return content
} catch {
print("WKWebViewInstrumentation. WKWebViewBodyCacheURLProtocol.getMatchedTextFromUrl() error: \(error)")
}
return nil
}
}
// MARK: - URLSessionDataDelegate
extension WKWebViewBodyCacheURLProtocol : URLSessionDataDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
self.clearRequestBody()
if let e = error {
self.client?.urlProtocol(self, didFailWithError: e)
} else {
self.client?.urlProtocolDidFinishLoading(self)
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.client?.urlProtocol(self, didLoad: data)
}
}
// MARK: - register scheme with browsingContextController for custom protocol
extension WKWebViewBodyCacheURLProtocol {
// encode with base64 for protection
private static var registerString: String {
return String(data: Data(base64Encoded: "cmVnaXN0ZXJTY2hlbWVGb3JDdXN0b21Qcm90b2NvbDo=")!, encoding: .utf8)!
}
// encode with base64 for protection
private static var unregisterString: String {
return String(data: Data(base64Encoded: "dW5yZWdpc3RlclNjaGVtZUZvckN1c3RvbVByb3RvY29sOg==")!, encoding: .utf8)!
}
// encode with base64 for protection
private static var controllerString: String {
return String(data: Data(base64Encoded: "YnJvd3NpbmdDb250ZXh0Q29udHJvbGxlcg==")!, encoding: .utf8)!
}
private static var browsingContextController : NSObject.Type? {
guard let instance = WKWebView().value(forKey: controllerString) else {
return nil
}
return type(of: instance) as? NSObject.Type
}
private static var registerSchemeForCustomProtocol : Selector {
return Selector((registerString))
}
private static var unregisterSchemeForCustomProtocol : Selector {
return Selector((unregisterString))
}
static func registerScheme(scheme: String) {
guard let controller = browsingContextController else {
return
}
if (controller.responds(to: registerSchemeForCustomProtocol)) {
controller.perform(registerSchemeForCustomProtocol, with: scheme)
}
}
static func unregisterScheme(scheme: String) {
guard let controller = browsingContextController else {
return
}
if (controller.responds(to: unregisterSchemeForCustomProtocol)) {
controller.perform(unregisterSchemeForCustomProtocol, with: scheme)
}
}
}