背景说明
之所以想hook XMLHTTPRequest是因为NSURLProtocol导流WKWebview有body丢失问题。要解决这个问题,目前一个思路,通过注入JS代码的方式来解决这个问题,在NSURLProtocol的didReceiveData函数中,我们可以第一时间接收到请求返回的HTML数据,然后我们把hook XMLHTTPRequest的send方法和open的JS函数插入到HTML之中,然后再返回给系统。
在hook代码里面要重组url和body参数并且加上标识位,然后重新发起get请求,然后这时候我们的NSURLProtocol会再次捕获这个重新发起的get请求,根据我们请求中的标记,辨别出是我们重组过的请求,将请求中的参数取出来(我们把body存放在文件里,做好标识,便于一一对应request,方便读取),再次拼接以后塞入body发起post请求。
1.Hook XMLHTTPRequest
我打算分成两步来进行,第一部分已经完成,第二部分还在进行中。
1.1 hook XMLHTTPRequest发起的请求
XMLHTTPRequest的hook,参考https://github.com/wendux/Ajax-hook 使用起来很简单,把hookAjax函数插入script标签就可以了。如下范例,
<script src="https://unpkg.com/ajax-hook/dist/ajaxhook.min.js"></script>///这行插入到HTML文件的head标签内
///如下内容插入到body标签内
<script>
hookAjax(
// hook functions and callbacks of XMLHttpRequest object
{
onreadystatechange: function (xhr) {
console.log("onreadystatechange called: %O", xhr)
//return true
},
onload: function (xhr) {
console.log("onload called: %O", xhr)
xhr.responseText = "hook" + xhr.responseText;
//return true;
},
open: function (arg, xhr) {
console.log("open called: method:%s,url:%s,async:%s", arg[0], arg[1], arg[2], xhr)
arg[1] += "?hook_tag=1";
//统一添加请求头
},
send: function (arg, xhr) {
console.log("send called: %O", arg[0])
xhr.setRequestHeader("_custom_header_", "ajaxhook")
},
setRequestHeader: function (arg, xhr) {
console.log("setRequestHeader called!", arg)
},
// hook attributes of XMLHttpRequest object
timeout: {
setter: function (v, xhr) {
//timeout shouldn't exceed 10s
return Math.max(v, 1000);
}
}
}
);
$.get().done(function (d) {
console.log(d.substr(0, 30) + "...")
//use original XMLHttpRequest
console.log("unhook")
unHookAjax()
$.get().done(function (d) {
console.log(d.substr(0, 10))
})
})
</script>
通过控制台的log,验证后,可以拦截到请求。
1.2 将hook代码接入NSURLProtocol
时间关系,这部分还在验证。这是下个阶段的任务。
2.另一种思路
XMLHTTPRequest是否有设置proxy的方式?如果可行的话,是不是就一步登天了?可惜,并不能,不过,有个老哥提出了另一种方式,https://juejin.im/post/5db85842f265da4d1f51bbda ,这种方式也算是hook的变种。
这种方式待验证.
update - 2023/7/26
时隔三年,ChatGPT横空出世,重新挖出这个问题,看看ChatGPT给的解决思路
当需要在WKWebView
中获取带有body的HTTP请求的body数据时,可以通过以下切实可行的方案实现JS和原生代码的交互:
-
在JS中获取请求的body数据:
在页面的JS中,可以通过XMLHttpRequest
或fetch
等方式发起HTTP请求,并将请求的body数据提取出来,然后将它传递给原生代码。为了实现这一点,你可以在JS中监听页面的网络请求,获取请求的body数据,并通过window.webkit.messageHandlers
与原生代码交互。例如,在JS中,可以这样获取请求的body数据:
// 发起XMLHttpRequest请求 var xhr = new XMLHttpRequest(); xhr.open("POST", "https://example.com/api", true); // 设置请求头,根据需要进行调整 xhr.setRequestHeader("Content-Type", "application/json"); // 设置请求完成的回调 xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { // 请求完成,将body数据传递给原生代码 var requestBody = xhr.responseText; // 这里假设请求返回的是JSON数据,如果是其他格式,需要相应调整 window.webkit.messageHandlers.getData.postMessage(requestBody); } }; // 发送请求,假设请求体为JSON格式 var requestBodyJSON = { "key": "value" }; var requestBodyString = JSON.stringify(requestBodyJSON); xhr.send(requestBodyString);
-
原生代码处理JS传递的body数据:
在原生代码中,你需要为WKWebView
设置一个JavaScript交互的接口,以便接收来自JS的数据。这可以通过WKWebView
的WKScriptMessageHandler
实现。首先,创建一个遵循
WKScriptMessageHandler
协议的自定义类,用于处理JS传递的数据:import UIKit import WebKit class MessageHandler: NSObject, WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if let requestBody = message.body as? String { // 在这里可以获取到JS传递的body数据 print("请求的body数据:", requestBody) } } }
-
将自定义MessageHandler与WKWebView关联:
在原生代码中,将自定义的MessageHandler
与WKWebView
关联,并添加需要交互的JavaScript脚本:import UIKit import WebKit class ViewController: UIViewController, WKNavigationDelegate { var webView: WKWebView! override func viewDidLoad() { super.viewDidLoad() // 创建WKWebView配置 let configuration = WKWebViewConfiguration() // 创建并关联MessageHandler let messageHandler = MessageHandler() let userContentController = WKUserContentController() userContentController.add(messageHandler, name: "getData") // 这里的名称要与JS中postMessage的名称一致 configuration.userContentController = userContentController // 创建WKWebView并加载页面 webView = WKWebView(frame: view.bounds, configuration: configuration) webView.navigationDelegate = self view.addSubview(webView) if let url = Bundle.main.url(forResource: "index", withExtension: "html") { webView.loadFileURL(url, allowingReadAccessTo: url) } } }
在JS中调用原生代码的接口:
在JS中,通过window.webkit.messageHandlers.getData.postMessage
调用原生代码处理请求的body数据。在原生代码中处理获取到的body数据:
在原生代码的MessageHandler
中,你可以将获取到的body数据进行进一步处理,或者传递给其他需要的地方。
通过以上步骤,你可以在WKWebView
中实现JS和原生代码的交互,从而获取到带有body的HTTP请求的body数据。请注意,由于涉及到与网页JS的交互,需要确保JS代码的正确性和安全性。另外,由于涉及到JS和原生代码的交互,如果你需要在多个地方使用这个功能,请考虑代码的复用性和整体的架构设计。