我最近在用 Node.js 开发一款命令行工具,名为 “Kup”。
Kup 底层使用 Got 这个库来发送请求,请求的接口都是 HTTPS 的。
我希望通过 Charles 来监听 Kup 与服务器之间的通信,完成一些调试工作。
本文理论上也适用于 Fiddler、Whistle 等基于代理的请求调试工具。
在本机安装 Charles 的 CA 证书,并设置为 “Always Trust”。
打开 Charles,启动 HTTP 代理(http://127.0.0.1:8888
),确认已开启 “SSL Proxying”。
(这也是 Charles 抓取浏览器端 HTTPS 请求的准备工作。)
把 Charles 代理设置为 macOS 全局代理。
在命令行运行 Kup。Charles 抓不到它发出的请求,失败。
取消全局代理,改为精准指定终端代理配置:
export https_proxy=http://127.0.0.1:8888
仍然抓不到,失败。
在上面两次失败的尝试中,似乎 Kup 压根就没有走 Charles 的代理。一番调研后发现,想用 Charles 抓 Node.js 请求,基本上有两种方案:
第一种是我们熟悉的常规方案:就是让我们的 Node.js 应用(比如 Kup)通过 Charles 提供的代理来发请求。这与 Charles 抓浏览器请求的原理是一样的。
第二种属于变通方案:让 Charles 启动一个反向代理,把真正的请求地址包装一层,然后把 Node.js 应用的请求指向 Charles 反向代理包装之后的地址。这样也可以完成对请求的抓取。
第二种方案的原理看起来更复杂,但操作上却是相对简单的。因为这个方案不需要在本机安装 CA 证书,Node.js 应用端也只需要改改接口地址就可以了。这个方法临时用用是可以的,但它的缺陷也很明显:Node.js 应用端需要考虑两套地址的切换问题,另外 Charles 这一端也需要做针对性的配置,整个方案的可移植性不好。遂放弃之。
第一种方案在实际操作上要麻烦得多。原因是 Node.js 应用在发送请求时并不会自动走环境变量 https_proxy
指定的代理,这个行为需要由应用自行实现,而这个实现过程往往有点原始:一个支持代理的 HTTP 请求库(比如 Got)通常不会直接集成代理功能,而是允许我们指定一个 http.Agent 实例来作为它的代理;于是我们又需要通过专门的库来创建 http.Agent 实例……
虽然麻烦了点,但所幸这些操作都集中在 Node.js 应用这一端,纯代码就可以搞定;而且我们对这种抓包原理也更熟悉,所以最终还是选它。
选好方案之后,实现起来似乎也不算很麻烦:
选用 https-proxy-agent 这个包来创建代理实例:
const HttpsProxyAgent = require('https-proxy-agent')
const agent = new HttpsProxyAgent('http://127.0.0.1:8888')
把这代理实例提供给 Got:
got(myApi, {
agent: {
https: agent, // 指定 HTTPS 代理
},
})
由于 Charles CA 证书并不是个正经的证书,我们还需要给 Got 加个配置,忽略对证书的校验:
got(myApi, {
agent: {
https: agent,
},
rejectUnauthorized: false, // 加这个选项
})
在此之后,每当 Kup 发出请求,Charles 就可以看到请求的具体情况了。成功!
当然,以上只是跑通流程的最小化实现。为了不影响用户的正常使用,上述代理行为需要限制在调试模式下,这些细节就不赘述了。
我在这个问题上也是一个学习者,本文记录了我的探索过程与决策过程,希望能提供一些参考价值。
抛砖引玉,如果大家有更好的方案,或者上面有错漏,请在评论区留言吧!